Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112    id: SketchCheckpointId,
113    source: SourceDelta,
114    program: Program,
115    scene_graph: SceneGraph,
116    exec_outcome: ExecOutcome,
117    point_freedom_cache: HashMap<ObjectId, Freedom>,
118    mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123    points: Vec<ObjectId>,
124    function_name: &'static str,
125    value: f64,
126    units: NumericSuffix,
127    label_position: Option<Point2d<Number>>,
128    constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143/// The name of the KCL std constant for a clockwise arc direction.
144const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190    Edit,
191    DeleteNonSketch,
192}
193
194/// Options that control how an edit is re-executed and written back.
195struct ExecuteAfterEditOptions {
196    segment_ids_edited: AhashIndexSet<ObjectId>,
197    edit_kind: EditDeleteKind,
198    commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202    /// Returns true if this edit is any type of deletion.
203    fn is_delete(&self) -> bool {
204        match self {
205            EditDeleteKind::Edit => false,
206            EditDeleteKind::DeleteNonSketch => true,
207        }
208    }
209
210    fn to_change_kind(self) -> ChangeKind {
211        match self {
212            EditDeleteKind::Edit => ChangeKind::Edit,
213            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220    Add,
221    Edit,
222    Delete,
223    None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230    #[serde(rename_all = "camelCase")]
231    Success {
232        scene_graph: Box<SceneGraph>,
233        exec_outcome: Box<ExecOutcome>,
234        checkpoint_id: Option<SketchCheckpointId>,
235    },
236    #[serde(rename_all = "camelCase")]
237    ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240/// Options for a sketch segment edit that participates in drag solving.
241pub struct EditSegmentsOptions {
242    /// Narrows which edited scene objects receive temporary fixed constraints.
243    ///
244    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
245    /// disables those fixed constraints, which is useful for semantic edits such
246    /// as toggling construction state.
247    pub anchor_segment_ids: Option<Vec<ObjectId>>,
248    /// Hidden fixed cursor points that the referenced segment bodies must pass
249    /// through during solve.
250    pub drag_anchors: Vec<SegmentDragAnchor>,
251    /// Constraint label positions to write in the same AST transaction as the
252    /// segment edits. Label positions do not participate in solving.
253    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254    /// Whether solver-updated initial guesses should be written back to KCL.
255    pub commit_solved_initial_guesses: bool,
256}
257
258/// Options for a distance-constraint label edit during sketch dragging.
259pub struct EditDistanceConstraintLabelPositionOptions {
260    /// Edited scene objects to keep anchored while previewing the label edit.
261    pub anchor_segment_ids: Vec<ObjectId>,
262    /// Whether solver-updated initial guesses should be written back to KCL.
263    pub commit_solved_initial_guesses: bool,
264}
265
266/// Options for editing a constraint during sketch dragging.
267pub struct EditConstraintOptions {
268    /// Whether solver-updated initial guesses should be written back to KCL.
269    pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274    variable_name: String,
275    output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280    program: Program,
281    scene_graph: SceneGraph,
282    /// Lightweight map from engine solid IDs to their latest KCL references.
283    solid_references: HashMap<Uuid, SolidAstReference>,
284    /// Stores the last known freedom value for each point object.
285    /// This allows us to preserve freedom values when freedom analysis isn't run.
286    point_freedom_cache: HashMap<ObjectId, Freedom>,
287    /// One-shot drag anchors for the next segment edit. These ids define which
288    /// edited points/segments become temporary fixed constraints during solve.
289    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290    /// One-shot segment-body drag anchors for the next segment edit. These add
291    /// a temporary solver point on the dragged segment that follows the cursor.
292    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293    /// One-shot constraint label edits to apply with the next segment edit.
294    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295    /// One-shot override for whether the next edit commits solver-updated
296    /// initial guesses back into KCL. Drag previews keep this off so only the
297    /// explicit drag edit feeds the next solve.
298    next_edit_commits_solver_solutions: Option<bool>,
299    sketch_checkpoints: VecDeque<SketchCheckpoint>,
300    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl FrontendState {
310    pub fn new() -> Self {
311        Self {
312            program: Program::empty(),
313            scene_graph: SceneGraph {
314                project: ProjectId(0),
315                file: FileId(0),
316                version: Version(0),
317                objects: Default::default(),
318                settings: Default::default(),
319                sketch_mode: Default::default(),
320            },
321            solid_references: HashMap::new(),
322            point_freedom_cache: HashMap::new(),
323            next_drag_anchor_segment_ids: None,
324            next_segment_drag_anchors: None,
325            next_constraint_label_edits: None,
326            next_edit_commits_solver_solutions: None,
327            sketch_checkpoints: VecDeque::new(),
328            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329        }
330    }
331
332    /// Get a reference to the scene graph
333    pub fn scene_graph(&self) -> &SceneGraph {
334        &self.scene_graph
335    }
336
337    pub fn default_length_unit(&self) -> UnitLength {
338        self.program
339            .meta_settings()
340            .ok()
341            .flatten()
342            .map(|settings| settings.default_length_units)
343            .unwrap_or(UnitLength::Millimeters)
344    }
345
346    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349        let checkpoint = SketchCheckpoint {
350            id: checkpoint_id,
351            source: SourceDelta {
352                text: source_from_ast(&self.program.ast),
353            },
354            program: self.program.clone(),
355            scene_graph: self.scene_graph.clone(),
356            exec_outcome,
357            point_freedom_cache: self.point_freedom_cache.clone(),
358            mock_memory: read_old_memory().await,
359        };
360
361        self.sketch_checkpoints.push_back(checkpoint);
362        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363            self.sketch_checkpoints.pop_front();
364        }
365
366        Ok(checkpoint_id)
367    }
368
369    /// Edit sketch segments with optional drag-solve overrides.
370    ///
371    /// Drag anchors add hidden fixed cursor points and constrain the referenced
372    /// segment bodies to pass through them, which lets body drags use the same
373    /// anchor model without pinning all child points. Preview callers disable
374    /// solver writeback so solved geometry can be returned without feeding every
375    /// solver value back into KCL.
376    pub async fn edit_segments_with_options(
377        &mut self,
378        ctx: &ExecutorContext,
379        version: Version,
380        sketch: ObjectId,
381        segments: Vec<ExistingSegmentCtor>,
382        options: EditSegmentsOptions,
383    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385            self.next_drag_anchor_segment_ids
386                .replace(anchor_ids.into_iter().collect())
387        });
388        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390        let previous_commit_mode = self
391            .next_edit_commits_solver_solutions
392            .replace(options.commit_solved_initial_guesses);
393        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394        if let Some(previous_anchor_ids) = previous_anchor_ids {
395            self.next_drag_anchor_segment_ids = previous_anchor_ids;
396        }
397        self.next_segment_drag_anchors = previous_drag_anchors;
398        self.next_constraint_label_edits = previous_constraint_label_edits;
399        self.next_edit_commits_solver_solutions = previous_commit_mode;
400        result
401    }
402
403    /// Edit a distance-constraint label position with optional solver writeback.
404    ///
405    /// Drag previews set `commit_solved_initial_guesses` to false so label
406    /// placement can be previewed against solved geometry without advancing
407    /// persistent KCL state until drag completion.
408    pub async fn edit_distance_constraint_label_position_with_options(
409        &mut self,
410        ctx: &ExecutorContext,
411        version: Version,
412        sketch: ObjectId,
413        constraint_id: ObjectId,
414        label_position: Point2d<Number>,
415        options: EditDistanceConstraintLabelPositionOptions,
416    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417        let previous_commit_mode = self
418            .next_edit_commits_solver_solutions
419            .replace(options.commit_solved_initial_guesses);
420        let result = SketchApi::edit_distance_constraint_label_position(
421            self,
422            ctx,
423            version,
424            sketch,
425            constraint_id,
426            label_position,
427            options.anchor_segment_ids,
428        )
429        .await;
430        self.next_edit_commits_solver_solutions = previous_commit_mode;
431        result
432    }
433
434    /// Edit a distance constraint with optional solver writeback.
435    pub async fn edit_distance_constraint_with_options(
436        &mut self,
437        ctx: &ExecutorContext,
438        version: Version,
439        sketch: ObjectId,
440        constraint_id: ObjectId,
441        constraint: Constraint,
442        options: EditConstraintOptions,
443    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445            .await
446    }
447
448    /// Edit an angle constraint with optional solver writeback.
449    pub async fn edit_angle_constraint_with_options(
450        &mut self,
451        ctx: &ExecutorContext,
452        version: Version,
453        sketch: ObjectId,
454        constraint_id: ObjectId,
455        angle: Angle,
456        options: EditConstraintOptions,
457    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459            .await
460    }
461
462    /// Edit an angle or distance-family constraint with optional solver writeback.
463    pub async fn edit_constraint_with_options(
464        &mut self,
465        ctx: &ExecutorContext,
466        _version: Version,
467        sketch: ObjectId,
468        constraint_id: ObjectId,
469        constraint: Constraint,
470        options: EditConstraintOptions,
471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472        // TODO: Check version.
473        let sketch_block_ref =
474            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478        })?;
479
480        let mut new_ast = self.program.ast.clone();
481        let command = match &object.kind {
482            ObjectKind::Constraint {
483                constraint:
484                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485            } => {
486                let (function_name, distance) = match &constraint {
487                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
488                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490                    _ => {
491                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
493                        )));
494                    }
495                };
496                let (call, value) = self
497                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498                    .map_err(KclErrorWithOutputs::no_outputs)?;
499                AstMutateCommand::EditDistanceConstraint { call, value }
500            }
501            ObjectKind::Constraint {
502                constraint: Constraint::Angle(_),
503            } => {
504                let Constraint::Angle(angle) = &constraint else {
505                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
507                    )));
508                };
509                let (call, value) = self
510                    .angle_constraint_ast_parts(angle, &mut new_ast)
511                    .map_err(KclErrorWithOutputs::no_outputs)?;
512                AstMutateCommand::EditAngleConstraint { call, value }
513            }
514            ObjectKind::Constraint { .. } => {
515                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516                    "Editing {} is not supported",
517                    object.kind.human_friendly_kind_with_article(),
518                ))));
519            }
520            _ => {
521                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522                    "Object is not a constraint: {constraint_id:?}"
523                ))));
524            }
525        };
526
527        self.mutate_ast(&mut new_ast, constraint_id, command)
528            .map_err(KclErrorWithOutputs::no_outputs)?;
529
530        self.execute_after_edit(
531            ctx,
532            sketch,
533            sketch_block_ref,
534            &mut new_ast,
535            ExecuteAfterEditOptions {
536                segment_ids_edited: Default::default(),
537                edit_kind: EditDeleteKind::Edit,
538                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539            },
540        )
541        .await
542    }
543
544    pub async fn restore_sketch_checkpoint(
545        &mut self,
546        checkpoint_id: SketchCheckpointId,
547    ) -> api::Result<RestoreSketchCheckpointOutcome> {
548        let checkpoint = self
549            .sketch_checkpoints
550            .iter()
551            .find(|checkpoint| checkpoint.id == checkpoint_id)
552            .cloned()
553            .ok_or_else(|| Error {
554                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555            })?;
556
557        self.program = checkpoint.program;
558        self.scene_graph = checkpoint.scene_graph.clone();
559        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560        self.point_freedom_cache = checkpoint.point_freedom_cache;
561        self.next_drag_anchor_segment_ids = None;
562        self.next_segment_drag_anchors = None;
563        self.next_constraint_label_edits = None;
564        self.next_edit_commits_solver_solutions = None;
565
566        if let Some(mock_memory) = checkpoint.mock_memory {
567            write_old_memory(mock_memory).await;
568        } else {
569            clear_mem_cache().await;
570        }
571
572        Ok(RestoreSketchCheckpointOutcome {
573            source_delta: checkpoint.source,
574            scene_graph_delta: SceneGraphDelta {
575                new_graph: self.scene_graph_for_ui(),
576                new_objects: Vec::new(),
577                invalidates_ids: true,
578                exec_outcome: checkpoint.exec_outcome,
579            },
580        })
581    }
582
583    pub fn clear_sketch_checkpoints(&mut self) {
584        self.sketch_checkpoints.clear();
585    }
586    fn scene_graph_for_ui(&self) -> SceneGraph {
587        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588            matches!(
589                object.kind,
590                ObjectKind::Segment {
591                    segment: Segment::ControlPointSpline(_)
592                }
593            )
594        });
595
596        if !has_control_point_splines {
597            return self.scene_graph.clone();
598        }
599
600        let hidden_constraint_ids = self
601            .scene_graph
602            .objects
603            .iter()
604            .filter_map(|object| match &object.kind {
605                ObjectKind::Constraint {
606                    constraint: Constraint::Coincident(coincident),
607                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608                    Some(object.id)
609                }
610                _ => None,
611            })
612            .collect::<HashSet<_>>();
613
614        if hidden_constraint_ids.is_empty() {
615            return self.scene_graph.clone();
616        }
617
618        let mut scene_graph = self.scene_graph.clone();
619        for object in &mut scene_graph.objects {
620            match &mut object.kind {
621                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622                    object.kind = ObjectKind::Nil;
623                }
624                ObjectKind::Sketch(sketch) => {
625                    sketch
626                        .constraints
627                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628                }
629                _ => {}
630            }
631        }
632
633        scene_graph
634    }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638    let mut first_owner_id = None;
639    for segment_id in coincident.segment_ids() {
640        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641            return false;
642        };
643
644        match first_owner_id {
645            Some(first_owner_id) if first_owner_id != owner_id => return false,
646            Some(_) => {}
647            None => first_owner_id = Some(owner_id),
648        }
649    }
650
651    first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655    let object = scene_graph.objects.get(segment_id.0)?;
656    let ObjectKind::Segment { segment } = &object.kind else {
657        return None;
658    };
659
660    match segment {
661        Segment::ControlPointSpline(_) => Some(segment_id),
662        Segment::Point(point) => point
663            .owner
664            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665        Segment::Line(line) => line
666            .owner
667            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668        _ => None,
669    }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673    matches!(
674        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675        Some(ObjectKind::Segment {
676            segment: Segment::ControlPointSpline(_)
677        })
678    )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682    let experimental_features_allowed = program
683        .meta_settings()
684        .ok()
685        .flatten()
686        .map(|settings| settings.experimental_features == WarningLevel::Allow)
687        .unwrap_or(false);
688    if experimental_features_allowed {
689        return Ok(program.clone());
690    }
691
692    program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696    async fn execute_mock(
697        &mut self,
698        ctx: &ExecutorContext,
699        _version: Version,
700        sketch: ObjectId,
701    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702        let sketch_block_ref =
703            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705        let mut truncated_program = self.program.clone();
706        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707            .map_err(KclErrorWithOutputs::no_outputs)?;
708
709        // Execute.
710        let outcome = ctx
711            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712            .await?;
713        let new_source = source_from_ast(&self.program.ast);
714        let src_delta = SourceDelta { text: new_source };
715        // MockConfig::default() has freedom_analysis: true
716        let outcome = self.update_state_after_exec(outcome, true);
717        let scene_graph_delta = SceneGraphDelta {
718            new_graph: self.scene_graph.clone(),
719            new_objects: Default::default(),
720            invalidates_ids: false,
721            exec_outcome: outcome,
722        };
723        Ok((src_delta, scene_graph_delta))
724    }
725
726    async fn new_sketch(
727        &mut self,
728        ctx: &ExecutorContext,
729        _project: ProjectId,
730        _file: FileId,
731        _version: Version,
732        args: SketchCtor,
733    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734        // TODO: Check version.
735
736        let mut new_ast = self.program.ast.clone();
737        // Create updated KCL source from args.
738        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739            .map_err(KclErrorWithOutputs::no_outputs)?;
740        let mut defined_names = find_defined_names(&new_ast);
741        let is_face_of_expr = matches!(
742            &plane_ast,
743            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744        );
745        if is_face_of_expr {
746            let face_name = next_free_name_with_padding("face", &defined_names)
747                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748            let face_decl = ast::VariableDeclaration::new(
749                ast::VariableDeclarator::new(&face_name, plane_ast),
750                ast::ItemVisibility::Default,
751                ast::VariableKind::Const,
752            );
753            new_ast
754                .body
755                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756                    face_decl,
757                ))));
758            defined_names.insert(face_name.clone());
759            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760        }
761        let sketch_ast = ast::SketchBlock {
762            arguments: vec![ast::LabeledArg {
763                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764                arg: plane_ast,
765            }],
766            body: Default::default(),
767            is_being_edited: false,
768            non_code_meta: Default::default(),
769            digest: None,
770        };
771        // Add a sketch block as a variable declaration directly, avoiding
772        // source-range mutation on a no-src node.
773        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775        let sketch_decl = ast::VariableDeclaration::new(
776            ast::VariableDeclarator::new(
777                &sketch_name,
778                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779            ),
780            ast::ItemVisibility::Default,
781            ast::VariableKind::Const,
782        );
783        new_ast
784            .body
785            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786                sketch_decl,
787            ))));
788        // Convert to string source to create real source ranges.
789        let new_source = source_from_ast(&new_ast);
790        // Parse the new source.
791        let new_program = parse_frontend_mutation_source(
792            &new_source,
793            "Error parsing KCL source after adding sketch",
794            "No AST produced after adding sketch",
795        )?;
796
797        // Make sure to only set this if there are no errors.
798        self.program = new_program.clone();
799
800        // We need to do an engine execute so that the plane object gets created
801        // and is cached.
802        let outcome = ctx.run_with_caching(new_program.clone()).await?;
803        let freedom_analysis_ran = true;
804
805        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807        let Some(sketch_id) = self
808            .scene_graph
809            .objects
810            .iter()
811            .filter_map(|object| match object.kind {
812                ObjectKind::Sketch(_) => Some(object.id),
813                _ => None,
814            })
815            .max_by_key(|id| id.0)
816        else {
817            return Err(KclErrorWithOutputs::from_error_outcome(
818                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819                outcome,
820            ));
821        };
822        // Store the object in the scene.
823        self.scene_graph.sketch_mode = Some(sketch_id);
824
825        let src_delta = SourceDelta { text: new_source };
826        let scene_graph_delta = SceneGraphDelta {
827            new_graph: self.scene_graph_for_ui(),
828            invalidates_ids: false,
829            new_objects: vec![sketch_id],
830            exec_outcome: outcome,
831        };
832        Ok((src_delta, scene_graph_delta, sketch_id))
833    }
834
835    async fn edit_sketch(
836        &mut self,
837        ctx: &ExecutorContext,
838        _project: ProjectId,
839        _file: FileId,
840        _version: Version,
841        sketch: ObjectId,
842    ) -> ExecResult<SceneGraphDelta> {
843        // TODO: Check version.
844
845        // Look up existing sketch.
846        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848        })?;
849        let ObjectKind::Sketch(_) = &sketch_object.kind else {
850            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851                "Object is not a sketch, it is {}",
852                sketch_object.kind.human_friendly_kind_with_article()
853            ))));
854        };
855        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857        // Enter sketch mode by setting the sketch_mode.
858        self.scene_graph.sketch_mode = Some(sketch);
859
860        // Truncate after the sketch block for mock execution.
861        let mut truncated_program = self.program.clone();
862        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863            .map_err(KclErrorWithOutputs::no_outputs)?;
864
865        // Execute in mock mode to ensure state is up to date. The caller will
866        // want freedom analysis to display segments correctly.
867        let outcome = ctx
868            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869            .await?;
870
871        // MockConfig::default() has freedom_analysis: true
872        let outcome = self.update_state_after_exec(outcome, true);
873        let scene_graph_delta = SceneGraphDelta {
874            new_graph: self.scene_graph_for_ui(),
875            invalidates_ids: false,
876            new_objects: Vec::new(),
877            exec_outcome: outcome,
878        };
879        Ok(scene_graph_delta)
880    }
881
882    async fn exit_sketch(
883        &mut self,
884        ctx: &ExecutorContext,
885        _version: Version,
886        sketch: ObjectId,
887    ) -> ExecResult<SceneGraph> {
888        // TODO: Check version.
889        #[cfg(not(target_arch = "wasm32"))]
890        let _ = sketch;
891        #[cfg(target_arch = "wasm32")]
892        if self.scene_graph.sketch_mode != Some(sketch) {
893            web_sys::console::warn_1(
894                &format!(
895                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896                    self.scene_graph.sketch_mode
897                )
898                .into(),
899            );
900        }
901        self.scene_graph.sketch_mode = None;
902
903        // Execute.
904        let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
907        self.update_state_after_exec(outcome, false);
908
909        Ok(self.scene_graph_for_ui())
910    }
911
912    async fn delete_sketch(
913        &mut self,
914        ctx: &ExecutorContext,
915        _version: Version,
916        sketch: ObjectId,
917    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918        // TODO: Check version.
919
920        let mut new_ast = self.program.ast.clone();
921
922        // Look up existing sketch.
923        let sketch_id = sketch;
924        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926        })?;
927        let ObjectKind::Sketch(_) = &sketch_object.kind else {
928            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929                "Object is not a sketch, it is {}",
930                sketch_object.kind.human_friendly_kind_with_article(),
931            ))));
932        };
933
934        // Modify the AST to remove the sketch.
935        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936            .map_err(KclErrorWithOutputs::no_outputs)?;
937
938        self.execute_after_delete_sketch(ctx, &mut new_ast).await
939    }
940
941    async fn add_segment(
942        &mut self,
943        ctx: &ExecutorContext,
944        _version: Version,
945        sketch: ObjectId,
946        segment: SegmentCtor,
947        _label: Option<String>,
948    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949        // TODO: Check version.
950        match segment {
951            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956        }
957    }
958
959    async fn edit_segments(
960        &mut self,
961        ctx: &ExecutorContext,
962        _version: Version,
963        sketch: ObjectId,
964        segments: Vec<ExistingSegmentCtor>,
965    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966        // TODO: Check version.
967        let sketch_block_ref =
968            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970        let mut new_ast = self.program.ast.clone();
971        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972        let mut invalidates_ids = false;
973
974        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
975        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
976        for segment in &segments {
977            edited_segment_ids.insert(segment.id);
978            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980                && let ObjectKind::Segment {
981                    segment: Segment::ControlPointSpline(existing_spline),
982                } = &existing_object.kind
983                && existing_spline.controls.len() != new_ctor.points.len()
984            {
985                invalidates_ids = true;
986            }
987        }
988        let drag_anchor_segment_ids = self
989            .next_drag_anchor_segment_ids
990            .take()
991            .unwrap_or_else(|| edited_segment_ids.clone());
992        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995        // Preprocess segments into a final_edits vector to handle if segments contains:
996        // - edit start point of line1 (as SegmentCtor::Point)
997        // - edit end point of line1 (as SegmentCtor::Point)
998        //
999        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1000        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1001        //
1002        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1003        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1004        // so the above example would result in a single line1 edit:
1005        // - the first start point edit creates a new line edit entry in final_edits
1006        // - the second end point edit finds this line edit and mutates the end position only.
1007        //
1008        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1009        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011        for segment in segments {
1012            let segment_id = segment.id;
1013            match segment.ctor {
1014                SegmentCtor::Point(ctor) => {
1015                    // Find the owner, if any (point -> line / arc)
1016                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017                        && let ObjectKind::Segment { segment } = &segment_object.kind
1018                        && let Segment::Point(point) = segment
1019                        && let Some(owner_id) = point.owner
1020                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022                    {
1023                        match owner_segment {
1024                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1026                                    let SegmentCtor::Line(line_ctor) = existing else {
1027                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028                                            "Internal: Expected line ctor for owner, but found {}",
1029                                            existing.human_friendly_kind_with_article()
1030                                        ))));
1031                                    };
1032                                    // Line owner is already in final_edits -> apply this point edit
1033                                    if line.start == segment_id {
1034                                        line_ctor.start = ctor.position;
1035                                    } else {
1036                                        line_ctor.end = ctor.position;
1037                                    }
1038                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039                                    // Line owner is not in final_edits yet -> create it
1040                                    let mut line_ctor = line_ctor.clone();
1041                                    if line.start == segment_id {
1042                                        line_ctor.start = ctor.position;
1043                                    } else {
1044                                        line_ctor.end = ctor.position;
1045                                    }
1046                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047                                } else {
1048                                    // This should never run..
1049                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050                                        "Internal: Line does not have line ctor, but found {}",
1051                                        line.ctor.human_friendly_kind_with_article()
1052                                    ))));
1053                                }
1054                                continue;
1055                            }
1056                            Segment::Arc(arc)
1057                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058                            {
1059                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1060                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1061                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062                                            "Internal: Expected arc ctor for owner, but found {}",
1063                                            existing.human_friendly_kind_with_article()
1064                                        ))));
1065                                    };
1066                                    if arc.start == segment_id {
1067                                        arc_ctor.start = ctor.position;
1068                                    } else if arc.end == segment_id {
1069                                        arc_ctor.end = ctor.position;
1070                                    } else {
1071                                        arc_ctor.center = ctor.position;
1072                                    }
1073                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074                                    let mut arc_ctor = arc_ctor.clone();
1075                                    if arc.start == segment_id {
1076                                        arc_ctor.start = ctor.position;
1077                                    } else if arc.end == segment_id {
1078                                        arc_ctor.end = ctor.position;
1079                                    } else {
1080                                        arc_ctor.center = ctor.position;
1081                                    }
1082                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083                                } else {
1084                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085                                        "Internal: Arc does not have arc ctor, but found {}",
1086                                        arc.ctor.human_friendly_kind_with_article()
1087                                    ))));
1088                                }
1089                                continue;
1090                            }
1091                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1093                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1094                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095                                            "Internal: Expected circle ctor for owner, but found {}",
1096                                            existing.human_friendly_kind_with_article()
1097                                        ))));
1098                                    };
1099                                    if circle.start == segment_id {
1100                                        circle_ctor.start = ctor.position;
1101                                    } else {
1102                                        circle_ctor.center = ctor.position;
1103                                    }
1104                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105                                    let mut circle_ctor = circle_ctor.clone();
1106                                    if circle.start == segment_id {
1107                                        circle_ctor.start = ctor.position;
1108                                    } else {
1109                                        circle_ctor.center = ctor.position;
1110                                    }
1111                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112                                } else {
1113                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                                        "Internal: Circle does not have circle ctor, but found {}",
1115                                        circle.ctor.human_friendly_kind_with_article()
1116                                    ))));
1117                                }
1118                                continue;
1119                            }
1120                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121                                let Some(control_index) =
1122                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1123                                else {
1124                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126                                    ))));
1127                                };
1128                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1129                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132                                            existing.human_friendly_kind_with_article()
1133                                        ))));
1134                                    };
1135                                    spline_ctor.points[control_index] = ctor.position;
1136                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137                                    let mut spline_ctor = spline_ctor.clone();
1138                                    spline_ctor.points[control_index] = ctor.position;
1139                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140                                } else {
1141                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143                                        spline.ctor.human_friendly_kind_with_article()
1144                                    ))));
1145                                }
1146                                continue;
1147                            }
1148                            _ => {}
1149                        }
1150                    }
1151
1152                    // No owner, it's an individual point
1153                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154                }
1155                SegmentCtor::Line(ctor) => {
1156                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157                }
1158                SegmentCtor::Arc(ctor) => {
1159                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160                }
1161                SegmentCtor::Circle(ctor) => {
1162                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163                }
1164                SegmentCtor::ControlPointSpline(ctor) => {
1165                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166                }
1167            }
1168        }
1169
1170        for (segment_id, ctor) in final_edits {
1171            match ctor {
1172                SegmentCtor::Point(ctor) => self
1173                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174                    .map_err(KclErrorWithOutputs::no_outputs)?,
1175                SegmentCtor::Line(ctor) => self
1176                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177                    .map_err(KclErrorWithOutputs::no_outputs)?,
1178                SegmentCtor::Arc(ctor) => self
1179                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180                    .map_err(KclErrorWithOutputs::no_outputs)?,
1181                SegmentCtor::Circle(ctor) => self
1182                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183                    .map_err(KclErrorWithOutputs::no_outputs)?,
1184                SegmentCtor::ControlPointSpline(ctor) => self
1185                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186                    .map_err(KclErrorWithOutputs::no_outputs)?,
1187            }
1188        }
1189        for edit in constraint_label_edits {
1190            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191                .map_err(KclErrorWithOutputs::no_outputs)?;
1192        }
1193        let (source_delta, mut scene_graph_delta) = self
1194            .execute_after_edit(
1195                ctx,
1196                sketch,
1197                sketch_block_ref,
1198                &mut new_ast,
1199                ExecuteAfterEditOptions {
1200                    segment_ids_edited: drag_anchor_segment_ids,
1201                    edit_kind: EditDeleteKind::Edit,
1202                    commit_solved_initial_guesses,
1203                },
1204            )
1205            .await?;
1206        if invalidates_ids {
1207            scene_graph_delta.invalidates_ids = true;
1208        }
1209        Ok((source_delta, scene_graph_delta))
1210    }
1211
1212    async fn delete_objects(
1213        &mut self,
1214        ctx: &ExecutorContext,
1215        _version: Version,
1216        sketch: ObjectId,
1217        constraint_ids: Vec<ObjectId>,
1218        segment_ids: Vec<ObjectId>,
1219    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220        // TODO: Check version.
1221        let sketch_block_ref =
1222            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224        // Deduplicate IDs.
1225        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1229        // also delete the point, as well as other points that are owned by the owner.
1230        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232        for segment_id in segment_ids_set.iter().copied() {
1233            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234                let ObjectKind::Segment { segment } = &segment_object.kind else {
1235                    return None;
1236                };
1237                match segment {
1238                    Segment::Point(point) => point.owner,
1239                    Segment::Line(line) => line.owner,
1240                    _ => None,
1241                }
1242            });
1243
1244            if let Some(owner_id) = owner_id
1245                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247                && matches!(
1248                    owner_segment,
1249                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250                )
1251            {
1252                // segment is owned -> delete the owner
1253                resolved_segment_ids_to_delete.insert(owner_id);
1254            } else {
1255                // segment is not owned by anything -> can be deleted
1256                resolved_segment_ids_to_delete.insert(segment_id);
1257            }
1258        }
1259        let referenced_constraint_ids = self
1260            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261            .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263        let mut new_ast = self.program.ast.clone();
1264
1265        for constraint_id in referenced_constraint_ids {
1266            if constraint_ids_set.contains(&constraint_id) {
1267                continue;
1268            }
1269
1270            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272            })?;
1273            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275                    "Object is not a constraint, it is {}",
1276                    constraint_object.kind.human_friendly_kind_with_article()
1277                ))));
1278            };
1279
1280            match constraint {
1281                Constraint::Coincident(coincident) => {
1282                    let remaining_segments =
1283                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1286                    if remaining_segments.len() >= 2 {
1287                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288                            .map_err(KclErrorWithOutputs::no_outputs)?;
1289                    } else {
1290                        constraint_ids_set.insert(constraint_id);
1291                    }
1292                }
1293                Constraint::EqualRadius(equal_radius) => {
1294                    let remaining_input = equal_radius
1295                        .input
1296                        .iter()
1297                        .copied()
1298                        .filter(|segment_id| {
1299                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300                        })
1301                        .collect::<Vec<_>>();
1302
1303                    if remaining_input.len() >= 2 {
1304                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305                            .map_err(KclErrorWithOutputs::no_outputs)?;
1306                    } else {
1307                        constraint_ids_set.insert(constraint_id);
1308                    }
1309                }
1310                Constraint::LinesEqualLength(lines_equal_length) => {
1311                    let remaining_lines = lines_equal_length
1312                        .lines
1313                        .iter()
1314                        .copied()
1315                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316                        .collect::<Vec<_>>();
1317
1318                    // Equal length constraint is only valid with at least 2 lines
1319                    if remaining_lines.len() >= 2 {
1320                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321                            .map_err(KclErrorWithOutputs::no_outputs)?;
1322                    } else {
1323                        constraint_ids_set.insert(constraint_id);
1324                    }
1325                }
1326                Constraint::Parallel(parallel) => {
1327                    let remaining_lines = parallel
1328                        .lines
1329                        .iter()
1330                        .copied()
1331                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332                        .collect::<Vec<_>>();
1333
1334                    if remaining_lines.len() >= 2 {
1335                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336                            .map_err(KclErrorWithOutputs::no_outputs)?;
1337                    } else {
1338                        constraint_ids_set.insert(constraint_id);
1339                    }
1340                }
1341                Constraint::Horizontal(Horizontal::Points { points }) => {
1342                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344                    if remaining_points.len() >= 2 {
1345                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346                            .map_err(KclErrorWithOutputs::no_outputs)?;
1347                    } else {
1348                        constraint_ids_set.insert(constraint_id);
1349                    }
1350                }
1351                Constraint::Vertical(Vertical::Points { points }) => {
1352                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354                    if remaining_points.len() >= 2 {
1355                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356                            .map_err(KclErrorWithOutputs::no_outputs)?;
1357                    } else {
1358                        constraint_ids_set.insert(constraint_id);
1359                    }
1360                }
1361                Constraint::Fixed(fixed) => {
1362                    if fixed.points.iter().any(|fixed_point| {
1363                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364                    }) {
1365                        constraint_ids_set.insert(constraint_id);
1366                    }
1367                }
1368                _ => {
1369                    // All other constraint types: if referenced by a segment -> delete the constraint
1370                    constraint_ids_set.insert(constraint_id);
1371                }
1372            }
1373        }
1374
1375        for constraint_id in constraint_ids_set {
1376            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377                .map_err(KclErrorWithOutputs::no_outputs)?;
1378        }
1379        for segment_id in resolved_segment_ids_to_delete {
1380            self.delete_segment(&mut new_ast, sketch, segment_id)
1381                .map_err(KclErrorWithOutputs::no_outputs)?;
1382        }
1383
1384        self.execute_after_edit(
1385            ctx,
1386            sketch,
1387            sketch_block_ref,
1388            &mut new_ast,
1389            ExecuteAfterEditOptions {
1390                segment_ids_edited: Default::default(),
1391                edit_kind: EditDeleteKind::DeleteNonSketch,
1392                commit_solved_initial_guesses: true,
1393            },
1394        )
1395        .await
1396    }
1397
1398    async fn add_constraint(
1399        &mut self,
1400        ctx: &ExecutorContext,
1401        _version: Version,
1402        sketch: ObjectId,
1403        constraint: Constraint,
1404    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405        // TODO: Check version.
1406
1407        // Save the original state as a backup - we'll restore it if anything fails
1408        let original_program = self.program.clone();
1409        let original_scene_graph = self.scene_graph.clone();
1410
1411        let mut new_ast = self.program.ast.clone();
1412        let sketch_block_ref = match constraint {
1413            Constraint::Coincident(coincident) => self
1414                .add_coincident(sketch, coincident, &mut new_ast)
1415                .await
1416                .map_err(KclErrorWithOutputs::no_outputs)?,
1417            Constraint::Distance(distance) => self
1418                .add_distance(sketch, distance, &mut new_ast)
1419                .await
1420                .map_err(KclErrorWithOutputs::no_outputs)?,
1421            Constraint::EqualRadius(equal_radius) => self
1422                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423                .await
1424                .map_err(KclErrorWithOutputs::no_outputs)?,
1425            Constraint::Fixed(fixed) => self
1426                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427                .await
1428                .map_err(KclErrorWithOutputs::no_outputs)?,
1429            Constraint::HorizontalDistance(distance) => self
1430                .add_horizontal_distance(sketch, distance, &mut new_ast)
1431                .await
1432                .map_err(KclErrorWithOutputs::no_outputs)?,
1433            Constraint::VerticalDistance(distance) => self
1434                .add_vertical_distance(sketch, distance, &mut new_ast)
1435                .await
1436                .map_err(KclErrorWithOutputs::no_outputs)?,
1437            Constraint::Horizontal(horizontal) => self
1438                .add_horizontal(sketch, horizontal, &mut new_ast)
1439                .await
1440                .map_err(KclErrorWithOutputs::no_outputs)?,
1441            Constraint::LinesEqualLength(lines_equal_length) => self
1442                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443                .await
1444                .map_err(KclErrorWithOutputs::no_outputs)?,
1445            Constraint::Midpoint(midpoint) => self
1446                .add_midpoint(sketch, midpoint, &mut new_ast)
1447                .await
1448                .map_err(KclErrorWithOutputs::no_outputs)?,
1449            Constraint::Parallel(parallel) => self
1450                .add_parallel(sketch, parallel, &mut new_ast)
1451                .await
1452                .map_err(KclErrorWithOutputs::no_outputs)?,
1453            Constraint::Perpendicular(perpendicular) => self
1454                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455                .await
1456                .map_err(KclErrorWithOutputs::no_outputs)?,
1457            Constraint::Radius(radius) => self
1458                .add_radius(sketch, radius, &mut new_ast)
1459                .await
1460                .map_err(KclErrorWithOutputs::no_outputs)?,
1461            Constraint::Diameter(diameter) => self
1462                .add_diameter(sketch, diameter, &mut new_ast)
1463                .await
1464                .map_err(KclErrorWithOutputs::no_outputs)?,
1465            Constraint::Symmetric(symmetric) => self
1466                .add_symmetric(sketch, symmetric, &mut new_ast)
1467                .await
1468                .map_err(KclErrorWithOutputs::no_outputs)?,
1469            Constraint::Vertical(vertical) => self
1470                .add_vertical(sketch, vertical, &mut new_ast)
1471                .await
1472                .map_err(KclErrorWithOutputs::no_outputs)?,
1473            Constraint::Angle(lines_at_angle) => self
1474                .add_angle(sketch, lines_at_angle, &mut new_ast)
1475                .await
1476                .map_err(KclErrorWithOutputs::no_outputs)?,
1477            Constraint::Tangent(tangent) => self
1478                .add_tangent(sketch, tangent, &mut new_ast)
1479                .await
1480                .map_err(KclErrorWithOutputs::no_outputs)?,
1481        };
1482
1483        let result = self
1484            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485            .await;
1486
1487        // If execution failed, restore the original state to prevent corruption
1488        if result.is_err() {
1489            self.program = original_program;
1490            self.scene_graph = original_scene_graph;
1491        }
1492
1493        result
1494    }
1495
1496    async fn chain_segment(
1497        &mut self,
1498        ctx: &ExecutorContext,
1499        version: Version,
1500        sketch: ObjectId,
1501        previous_segment_end_point_id: ObjectId,
1502        segment: SegmentCtor,
1503        _label: Option<String>,
1504    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505        // TODO: Check version.
1506
1507        // First, add the segment (line) to get its start point ID
1508        let SegmentCtor::Line(line_ctor) = segment else {
1509            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510                "chain_segment currently only supports Line segments, got {}",
1511                segment.human_friendly_kind_with_article(),
1512            ))));
1513        };
1514
1515        // Add the line segment first - this updates self.program and self.scene_graph
1516        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518        // Find the new line's start point ID from the updated scene graph
1519        // add_line updates self.scene_graph, so we can use that
1520        let new_line_id = first_scene_delta
1521            .new_objects
1522            .iter()
1523            .find(|&obj_id| {
1524                let obj = self.scene_graph.objects.get(obj_id.0);
1525                if let Some(obj) = obj {
1526                    matches!(
1527                        &obj.kind,
1528                        ObjectKind::Segment {
1529                            segment: Segment::Line(_)
1530                        }
1531                    )
1532                } else {
1533                    false
1534                }
1535            })
1536            .ok_or_else(|| {
1537                KclErrorWithOutputs::no_outputs(KclError::refactor(
1538                    "Failed to find new line segment in scene graph".to_string(),
1539                ))
1540            })?;
1541
1542        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "New line object not found: {new_line_id:?}"
1545            )))
1546        })?;
1547
1548        let ObjectKind::Segment {
1549            segment: new_line_segment,
1550        } = &new_line_obj.kind
1551        else {
1552            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553                "Object is not a segment: {new_line_obj:?}"
1554            ))));
1555        };
1556
1557        let Segment::Line(new_line) = new_line_segment else {
1558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559                "Segment is not a line: {new_line_segment:?}"
1560            ))));
1561        };
1562
1563        let new_line_start_point_id = new_line.start;
1564
1565        // Now add the coincident constraint between the previous end point and the new line's start point.
1566        let coincident = Coincident {
1567            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568        };
1569
1570        let (final_src_delta, final_scene_delta) = self
1571            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572            .await?;
1573
1574        // Combine new objects from the line addition and the constraint addition.
1575        // Both add_line and add_constraint now populate new_objects correctly.
1576        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577        combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579        let scene_graph_delta = SceneGraphDelta {
1580            new_graph: self.scene_graph_for_ui(),
1581            invalidates_ids: false,
1582            new_objects: combined_new_objects,
1583            exec_outcome: final_scene_delta.exec_outcome,
1584        };
1585
1586        Ok((final_src_delta, scene_graph_delta))
1587    }
1588
1589    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1590    async fn edit_constraint_value(
1591        &mut self,
1592        ctx: &ExecutorContext,
1593        _version: Version,
1594        sketch: ObjectId,
1595        constraint_id: ObjectId,
1596        value_expression: String,
1597    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598        // TODO: Check version.
1599        let sketch_block_ref =
1600            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604        })?;
1605        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607                "Object is not a constraint: {constraint_id:?}"
1608            ))));
1609        }
1610
1611        let mut new_ast = self.program.ast.clone();
1612
1613        // Parse the expression string into an AST node.
1614        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616                "Invalid constraint value",
1617                &e,
1618            )))
1619        })?;
1620        if !errors.is_empty() {
1621            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622                format_compilation_issues("Invalid constraint value", &errors),
1623            )));
1624        }
1625        let mut parsed = parsed.ok_or_else(|| {
1626            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627        })?;
1628        if parsed.ast.body.is_empty() {
1629            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630                "Empty value expression".to_string(),
1631            )));
1632        }
1633        let first = parsed.ast.body.remove(0);
1634        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636                "Value expression must be a simple expression".to_string(),
1637            )));
1638        };
1639
1640        let new_value: ast::BinaryPart = expr_stmt
1641            .inner
1642            .expression
1643            .try_into()
1644            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646        self.mutate_ast(
1647            &mut new_ast,
1648            constraint_id,
1649            AstMutateCommand::EditConstraintValue { value: new_value },
1650        )
1651        .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653        self.execute_after_edit(
1654            ctx,
1655            sketch,
1656            sketch_block_ref,
1657            &mut new_ast,
1658            ExecuteAfterEditOptions {
1659                segment_ids_edited: Default::default(),
1660                edit_kind: EditDeleteKind::Edit,
1661                commit_solved_initial_guesses: true,
1662            },
1663        )
1664        .await
1665    }
1666
1667    async fn edit_distance_constraint_label_position(
1668        &mut self,
1669        ctx: &ExecutorContext,
1670        _version: Version,
1671        sketch: ObjectId,
1672        constraint_id: ObjectId,
1673        label_position: Point2d<Number>,
1674        anchor_segment_ids: Vec<ObjectId>,
1675    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676        // TODO: Check version.
1677        let sketch_block_ref =
1678            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680        let mut new_ast = self.program.ast.clone();
1681        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682            .map_err(KclErrorWithOutputs::no_outputs)?;
1683        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685        self.execute_after_edit(
1686            ctx,
1687            sketch,
1688            sketch_block_ref,
1689            &mut new_ast,
1690            ExecuteAfterEditOptions {
1691                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692                edit_kind: EditDeleteKind::Edit,
1693                commit_solved_initial_guesses,
1694            },
1695        )
1696        .await
1697    }
1698
1699    /// Splitting a segment means creating a new segment, editing the old one, and then
1700    /// migrating a bunch of the constraints from the original segment to the new one
1701    /// (i.e. deleting them and re-adding them on the other segment).
1702    ///
1703    /// To keep this efficient we require as few executions as possible: we create the
1704    /// new segment first (to get its id), then do all edits and new constraints, and
1705    /// do all deletes at the end (since deletes invalidate ids).
1706    async fn batch_split_segment_operations(
1707        &mut self,
1708        ctx: &ExecutorContext,
1709        _version: Version,
1710        sketch: ObjectId,
1711        edit_segments: Vec<ExistingSegmentCtor>,
1712        add_constraints: Vec<Constraint>,
1713        delete_constraint_ids: Vec<ObjectId>,
1714        _new_segment_info: sketch::NewSegmentInfo,
1715    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716        // TODO: Check version.
1717        let sketch_block_ref =
1718            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720        let mut new_ast = self.program.ast.clone();
1721        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723        // Step 1: Edit segments
1724        for segment in edit_segments {
1725            segment_ids_edited.insert(segment.id);
1726            match segment.ctor {
1727                SegmentCtor::Point(ctor) => self
1728                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729                    .map_err(KclErrorWithOutputs::no_outputs)?,
1730                SegmentCtor::Line(ctor) => self
1731                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732                    .map_err(KclErrorWithOutputs::no_outputs)?,
1733                SegmentCtor::Arc(ctor) => self
1734                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735                    .map_err(KclErrorWithOutputs::no_outputs)?,
1736                SegmentCtor::Circle(ctor) => self
1737                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738                    .map_err(KclErrorWithOutputs::no_outputs)?,
1739                SegmentCtor::ControlPointSpline(ctor) => self
1740                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741                    .map_err(KclErrorWithOutputs::no_outputs)?,
1742            }
1743        }
1744
1745        // Step 2: Add all constraints
1746        for constraint in add_constraints {
1747            match constraint {
1748                Constraint::Coincident(coincident) => {
1749                    self.add_coincident(sketch, coincident, &mut new_ast)
1750                        .await
1751                        .map_err(KclErrorWithOutputs::no_outputs)?;
1752                }
1753                Constraint::Distance(distance) => {
1754                    self.add_distance(sketch, distance, &mut new_ast)
1755                        .await
1756                        .map_err(KclErrorWithOutputs::no_outputs)?;
1757                }
1758                Constraint::EqualRadius(equal_radius) => {
1759                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760                        .await
1761                        .map_err(KclErrorWithOutputs::no_outputs)?;
1762                }
1763                Constraint::Fixed(fixed) => {
1764                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765                        .await
1766                        .map_err(KclErrorWithOutputs::no_outputs)?;
1767                }
1768                Constraint::HorizontalDistance(distance) => {
1769                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770                        .await
1771                        .map_err(KclErrorWithOutputs::no_outputs)?;
1772                }
1773                Constraint::VerticalDistance(distance) => {
1774                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1775                        .await
1776                        .map_err(KclErrorWithOutputs::no_outputs)?;
1777                }
1778                Constraint::Horizontal(horizontal) => {
1779                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1780                        .await
1781                        .map_err(KclErrorWithOutputs::no_outputs)?;
1782                }
1783                Constraint::LinesEqualLength(lines_equal_length) => {
1784                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785                        .await
1786                        .map_err(KclErrorWithOutputs::no_outputs)?;
1787                }
1788                Constraint::Midpoint(midpoint) => {
1789                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                Constraint::Parallel(parallel) => {
1794                    self.add_parallel(sketch, parallel, &mut new_ast)
1795                        .await
1796                        .map_err(KclErrorWithOutputs::no_outputs)?;
1797                }
1798                Constraint::Perpendicular(perpendicular) => {
1799                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800                        .await
1801                        .map_err(KclErrorWithOutputs::no_outputs)?;
1802                }
1803                Constraint::Vertical(vertical) => {
1804                    self.add_vertical(sketch, vertical, &mut new_ast)
1805                        .await
1806                        .map_err(KclErrorWithOutputs::no_outputs)?;
1807                }
1808                Constraint::Diameter(diameter) => {
1809                    self.add_diameter(sketch, diameter, &mut new_ast)
1810                        .await
1811                        .map_err(KclErrorWithOutputs::no_outputs)?;
1812                }
1813                Constraint::Radius(radius) => {
1814                    self.add_radius(sketch, radius, &mut new_ast)
1815                        .await
1816                        .map_err(KclErrorWithOutputs::no_outputs)?;
1817                }
1818                Constraint::Symmetric(symmetric) => {
1819                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1820                        .await
1821                        .map_err(KclErrorWithOutputs::no_outputs)?;
1822                }
1823                Constraint::Angle(angle) => {
1824                    self.add_angle(sketch, angle, &mut new_ast)
1825                        .await
1826                        .map_err(KclErrorWithOutputs::no_outputs)?;
1827                }
1828                Constraint::Tangent(tangent) => {
1829                    self.add_tangent(sketch, tangent, &mut new_ast)
1830                        .await
1831                        .map_err(KclErrorWithOutputs::no_outputs)?;
1832                }
1833            }
1834        }
1835
1836        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1837        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839        let has_constraint_deletions = !constraint_ids_set.is_empty();
1840        for constraint_id in constraint_ids_set {
1841            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842                .map_err(KclErrorWithOutputs::no_outputs)?;
1843        }
1844
1845        // Step 4: Execute once at the end
1846        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1847        // But we'll manually set invalidates_ids: true if we deleted constraints
1848        let (source_delta, mut scene_graph_delta) = self
1849            .execute_after_edit(
1850                ctx,
1851                sketch,
1852                sketch_block_ref,
1853                &mut new_ast,
1854                ExecuteAfterEditOptions {
1855                    segment_ids_edited,
1856                    edit_kind: EditDeleteKind::Edit,
1857                    commit_solved_initial_guesses: true,
1858                },
1859            )
1860            .await?;
1861
1862        // If we deleted constraints, set invalidates_ids: true
1863        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1864        if has_constraint_deletions {
1865            scene_graph_delta.invalidates_ids = true;
1866        }
1867
1868        Ok((source_delta, scene_graph_delta))
1869    }
1870
1871    async fn batch_tail_cut_operations(
1872        &mut self,
1873        ctx: &ExecutorContext,
1874        _version: Version,
1875        sketch: ObjectId,
1876        edit_segments: Vec<ExistingSegmentCtor>,
1877        add_constraints: Vec<Constraint>,
1878        delete_constraint_ids: Vec<ObjectId>,
1879        additional_edited_segment_ids: Vec<ObjectId>,
1880    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881        let sketch_block_ref =
1882            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884        let mut new_ast = self.program.ast.clone();
1885        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887        // Step 1: Edit segments (usually a single segment for tail cut)
1888        for segment in edit_segments {
1889            segment_ids_edited.insert(segment.id);
1890            match segment.ctor {
1891                SegmentCtor::Point(ctor) => self
1892                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893                    .map_err(KclErrorWithOutputs::no_outputs)?,
1894                SegmentCtor::Line(ctor) => self
1895                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896                    .map_err(KclErrorWithOutputs::no_outputs)?,
1897                SegmentCtor::Arc(ctor) => self
1898                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899                    .map_err(KclErrorWithOutputs::no_outputs)?,
1900                SegmentCtor::Circle(ctor) => self
1901                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902                    .map_err(KclErrorWithOutputs::no_outputs)?,
1903                SegmentCtor::ControlPointSpline(ctor) => self
1904                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905                    .map_err(KclErrorWithOutputs::no_outputs)?,
1906            }
1907        }
1908
1909        segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911        // Step 2: Add coincident constraints
1912        for constraint in add_constraints {
1913            match constraint {
1914                Constraint::Coincident(coincident) => {
1915                    self.add_coincident(sketch, coincident, &mut new_ast)
1916                        .await
1917                        .map_err(KclErrorWithOutputs::no_outputs)?;
1918                }
1919                other => {
1920                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921                        "unsupported constraint in tail cut batch: {other:?}"
1922                    ))));
1923                }
1924            }
1925        }
1926
1927        // Step 3: Delete constraints (if any)
1928        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930        let has_constraint_deletions = !constraint_ids_set.is_empty();
1931        for constraint_id in constraint_ids_set {
1932            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933                .map_err(KclErrorWithOutputs::no_outputs)?;
1934        }
1935
1936        // Step 4: Single execute_after_edit
1937        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1938        // But we'll manually set invalidates_ids: true if we deleted constraints
1939        let (source_delta, mut scene_graph_delta) = self
1940            .execute_after_edit(
1941                ctx,
1942                sketch,
1943                sketch_block_ref,
1944                &mut new_ast,
1945                ExecuteAfterEditOptions {
1946                    segment_ids_edited,
1947                    edit_kind: EditDeleteKind::Edit,
1948                    commit_solved_initial_guesses: true,
1949                },
1950            )
1951            .await?;
1952
1953        // If we deleted constraints, set invalidates_ids: true
1954        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1955        if has_constraint_deletions {
1956            scene_graph_delta.invalidates_ids = true;
1957        }
1958
1959        Ok((source_delta, scene_graph_delta))
1960    }
1961}
1962
1963impl FrontendState {
1964    /// Evaluate an input expression using the current model's settings and cached variables.
1965    pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1966        program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1967        ctx.run_mock(&program, &MockConfig::default()).await
1968    }
1969
1970    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1971        self.program = program.clone();
1972
1973        // Execute so that the objects are updated and available for the next
1974        // API call.
1975        // This always uses engine execution (not mock) so that things are cached.
1976        // Engine execution now runs freedom analysis automatically.
1977        // Keep existing checkpoints alive here. History may still reference
1978        // older committed sketch states across a direct-edit boundary, and a
1979        // checkpoint restore is a full state replacement anyway. We append a
1980        // fresh baseline checkpoint after the full execution below.
1981        // Clear the freedom cache since IDs might have changed after direct editing
1982        // and we're about to run freedom analysis which will repopulate it.
1983        self.point_freedom_cache.clear();
1984        match ctx.run_with_caching(program).await {
1985            Ok(outcome) => {
1986                let outcome = self.update_state_after_exec(outcome, true);
1987                let checkpoint_id = self
1988                    .create_sketch_checkpoint(outcome.clone())
1989                    .await
1990                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1991                Ok(SetProgramOutcome::Success {
1992                    scene_graph: Box::new(self.scene_graph_for_ui()),
1993                    exec_outcome: Box::new(outcome),
1994                    checkpoint_id: Some(checkpoint_id),
1995                })
1996            }
1997            Err(mut err) => {
1998                // Don't return an error just because execution failed. Instead,
1999                // update state as much as possible.
2000                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2001                self.update_state_after_exec(outcome, true);
2002                err.scene_graph = Some(self.scene_graph_for_ui());
2003                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2004            }
2005        }
2006    }
2007
2008    /// Decorate engine execution such that our state is updated and the scene
2009    /// graph is added to the return.
2010    pub async fn engine_execute(
2011        &mut self,
2012        ctx: &ExecutorContext,
2013        program: Program,
2014    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2015        self.program = program.clone();
2016
2017        // Engine execution now runs freedom analysis automatically. Clear the
2018        // freedom cache since IDs might have changed after direct editing, and
2019        // we're about to run freedom analysis which will repopulate it.
2020        self.point_freedom_cache.clear();
2021        match ctx.run_with_caching(program).await {
2022            Ok(outcome) => {
2023                let outcome = self.update_state_after_exec(outcome, true);
2024                Ok(SceneGraphDelta {
2025                    new_graph: self.scene_graph_for_ui(),
2026                    exec_outcome: outcome,
2027                    // We don't know what the new objects are.
2028                    new_objects: Default::default(),
2029                    // We don't know if IDs were invalidated.
2030                    invalidates_ids: Default::default(),
2031                })
2032            }
2033            Err(mut err) => {
2034                // Update state as much as possible, even when there's an error.
2035                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2036                self.update_state_after_exec(outcome, true);
2037                err.scene_graph = Some(self.scene_graph_for_ui());
2038                Err(err)
2039            }
2040        }
2041    }
2042
2043    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2044        if matches!(err.error, KclError::EngineHangup { .. }) {
2045            // It's not ideal to special-case this, but this error is very
2046            // common during development, and it causes confusing downstream
2047            // errors that have nothing to do with the actual problem.
2048            return Err(err);
2049        }
2050
2051        let KclErrorWithOutputs {
2052            error,
2053            mut non_fatal,
2054            variables,
2055            operations,
2056            artifact_graph,
2057            scene_objects,
2058            source_range_to_object,
2059            var_solutions,
2060            refactor_metadata,
2061            filenames,
2062            source_files,
2063            default_planes,
2064            ..
2065        } = err;
2066
2067        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2068
2069        Ok(ExecOutcome {
2070            variables,
2071            #[cfg(test)]
2072            test_program_memory: Default::default(),
2073            filenames,
2074            operations,
2075            artifact_graph,
2076            scene_objects,
2077            source_range_to_object,
2078            var_solutions,
2079            refactor_metadata,
2080            issues: non_fatal,
2081            source_files,
2082            default_planes,
2083        })
2084    }
2085
2086    async fn add_point(
2087        &mut self,
2088        ctx: &ExecutorContext,
2089        sketch: ObjectId,
2090        ctor: PointCtor,
2091    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2092        // Create updated KCL source from args.
2093        let at_ast = to_ast_point2d(&ctor.position)
2094            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2095        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2096            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2097            unlabeled: None,
2098            arguments: vec![ast::LabeledArg {
2099                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2100                arg: at_ast,
2101            }],
2102            digest: None,
2103            non_code_meta: Default::default(),
2104        })));
2105
2106        // Look up existing sketch.
2107        let sketch_id = sketch;
2108        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2109            #[cfg(target_arch = "wasm32")]
2110            web_sys::console::error_1(
2111                &format!(
2112                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2113                    self.scene_graph.objects
2114                )
2115                .into(),
2116            );
2117            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2118        })?;
2119        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2120            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2121                "Object is not a sketch, it is {}",
2122                sketch_object.kind.human_friendly_kind_with_article(),
2123            ))));
2124        };
2125        // Add the point to the AST of the sketch block.
2126        let mut new_ast = self.program.ast.clone();
2127        let (sketch_block_ref, _) = self
2128            .mutate_ast(
2129                &mut new_ast,
2130                sketch_id,
2131                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2132            )
2133            .map_err(KclErrorWithOutputs::no_outputs)?;
2134        // Convert to string source to create real source ranges.
2135        let new_source = source_from_ast(&new_ast);
2136        // Parse the new KCL source.
2137        let new_program = parse_frontend_mutation_source(
2138            &new_source,
2139            "Error parsing KCL source after adding point",
2140            "No AST produced after adding point",
2141        )?;
2142
2143        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2144            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2145                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2146            )))
2147        })?;
2148
2149        // Make sure to only set this if there are no errors.
2150        self.program = new_program.clone();
2151
2152        // Truncate after the sketch block for mock execution.
2153        let mut truncated_program = new_program;
2154        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2155            .map_err(KclErrorWithOutputs::no_outputs)?;
2156
2157        // Execute.
2158        let outcome = ctx
2159            .run_mock(
2160                &truncated_program,
2161                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2162            )
2163            .await?;
2164
2165        let new_object_ids = {
2166            let make_err =
2167                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2168            let segment_id = outcome
2169                .source_range_to_object
2170                .get(&point_node_ref.range)
2171                .copied()
2172                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2173            let segment_object = outcome
2174                .scene_objects
2175                .get(segment_id.0)
2176                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2177            let ObjectKind::Segment { segment } = &segment_object.kind else {
2178                return Err(make_err(format!(
2179                    "Object is not a segment, it is {}",
2180                    segment_object.kind.human_friendly_kind_with_article()
2181                )));
2182            };
2183            let Segment::Point(_) = segment else {
2184                return Err(make_err(format!(
2185                    "Segment is not a point, it is {}",
2186                    segment.human_friendly_kind_with_article()
2187                )));
2188            };
2189            vec![segment_id]
2190        };
2191        let src_delta = SourceDelta { text: new_source };
2192        // Uses .no_freedom_analysis() so freedom_analysis: false
2193        let outcome = self.update_state_after_exec(outcome, false);
2194        let scene_graph_delta = SceneGraphDelta {
2195            new_graph: self.scene_graph_for_ui(),
2196            invalidates_ids: false,
2197            new_objects: new_object_ids,
2198            exec_outcome: outcome,
2199        };
2200        Ok((src_delta, scene_graph_delta))
2201    }
2202
2203    async fn add_line(
2204        &mut self,
2205        ctx: &ExecutorContext,
2206        sketch: ObjectId,
2207        ctor: LineCtor,
2208    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2209        // Create updated KCL source from args.
2210        let start_ast = to_ast_point2d(&ctor.start)
2211            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2212        let end_ast = to_ast_point2d(&ctor.end)
2213            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2214        let mut arguments = vec![
2215            ast::LabeledArg {
2216                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2217                arg: start_ast,
2218            },
2219            ast::LabeledArg {
2220                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2221                arg: end_ast,
2222            },
2223        ];
2224        // Add construction kwarg if construction is Some(true)
2225        if ctor.construction == Some(true) {
2226            arguments.push(ast::LabeledArg {
2227                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2228                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2229                    value: ast::LiteralValue::Bool(true),
2230                    raw: "true".to_string(),
2231                    digest: None,
2232                }))),
2233            });
2234        }
2235        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2236            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2237            unlabeled: None,
2238            arguments,
2239            digest: None,
2240            non_code_meta: Default::default(),
2241        })));
2242
2243        // Look up existing sketch.
2244        let sketch_id = sketch;
2245        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2246            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2247        })?;
2248        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2249            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2250                "Object is not a sketch, it is {}",
2251                sketch_object.kind.human_friendly_kind_with_article(),
2252            ))));
2253        };
2254        // Add the line to the AST of the sketch block.
2255        let mut new_ast = self.program.ast.clone();
2256        let (sketch_block_ref, _) = self
2257            .mutate_ast(
2258                &mut new_ast,
2259                sketch_id,
2260                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2261            )
2262            .map_err(KclErrorWithOutputs::no_outputs)?;
2263        // Convert to string source to create real source ranges.
2264        let new_source = source_from_ast(&new_ast);
2265        // Parse the new KCL source.
2266        let new_program = parse_frontend_mutation_source(
2267            &new_source,
2268            "Error parsing KCL source after adding line",
2269            "No AST produced after adding line",
2270        )?;
2271
2272        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2273            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2274                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2275            )))
2276        })?;
2277
2278        // Make sure to only set this if there are no errors.
2279        self.program = new_program.clone();
2280
2281        // Truncate after the sketch block for mock execution.
2282        let mut truncated_program = new_program;
2283        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2284            .map_err(KclErrorWithOutputs::no_outputs)?;
2285
2286        // Execute.
2287        let outcome = ctx
2288            .run_mock(
2289                &truncated_program,
2290                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2291            )
2292            .await?;
2293
2294        let new_object_ids = {
2295            let make_err =
2296                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2297            let segment_id = outcome
2298                .source_range_to_object
2299                .get(&line_node_ref.range)
2300                .copied()
2301                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2302            let segment_object = outcome
2303                .scene_object_by_id(segment_id)
2304                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2305            let ObjectKind::Segment { segment } = &segment_object.kind else {
2306                return Err(make_err(format!(
2307                    "Object is not a segment, it is {}",
2308                    segment_object.kind.human_friendly_kind_with_article()
2309                )));
2310            };
2311            let Segment::Line(line) = segment else {
2312                return Err(make_err(format!(
2313                    "Segment is not a line, it is {}",
2314                    segment.human_friendly_kind_with_article()
2315                )));
2316            };
2317            vec![line.start, line.end, segment_id]
2318        };
2319        let src_delta = SourceDelta { text: new_source };
2320        // Uses .no_freedom_analysis() so freedom_analysis: false
2321        let outcome = self.update_state_after_exec(outcome, false);
2322        let scene_graph_delta = SceneGraphDelta {
2323            new_graph: self.scene_graph_for_ui(),
2324            invalidates_ids: false,
2325            new_objects: new_object_ids,
2326            exec_outcome: outcome,
2327        };
2328        Ok((src_delta, scene_graph_delta))
2329    }
2330
2331    async fn add_arc(
2332        &mut self,
2333        ctx: &ExecutorContext,
2334        sketch: ObjectId,
2335        ctor: ArcCtor,
2336    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2337        // Create updated KCL source from args.
2338        let start_ast = to_ast_point2d(&ctor.start)
2339            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2340        let end_ast = to_ast_point2d(&ctor.end)
2341            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2342        let center_ast = to_ast_point2d(&ctor.center)
2343            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2344        let mut arguments = vec![
2345            ast::LabeledArg {
2346                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2347                arg: start_ast,
2348            },
2349            ast::LabeledArg {
2350                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2351                arg: end_ast,
2352            },
2353            ast::LabeledArg {
2354                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2355                arg: center_ast,
2356            },
2357        ];
2358        // Add direction kwarg if it's clockwise, since counterclockwise is the
2359        // default.
2360        if ctor.direction == Some(ArcDirection::Cw) {
2361            arguments.push(ast::LabeledArg {
2362                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2363                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2364            });
2365        }
2366        // Add construction kwarg if construction is Some(true)
2367        if ctor.construction == Some(true) {
2368            arguments.push(ast::LabeledArg {
2369                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2370                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2371                    value: ast::LiteralValue::Bool(true),
2372                    raw: "true".to_string(),
2373                    digest: None,
2374                }))),
2375            });
2376        }
2377        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2378            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2379            unlabeled: None,
2380            arguments,
2381            digest: None,
2382            non_code_meta: Default::default(),
2383        })));
2384
2385        // Look up existing sketch.
2386        let sketch_id = sketch;
2387        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2388            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2389        })?;
2390        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2391            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2392                "Object is not a sketch, it is {}",
2393                sketch_object.kind.human_friendly_kind_with_article(),
2394            ))));
2395        };
2396        // Add the arc to the AST of the sketch block.
2397        let mut new_ast = self.program.ast.clone();
2398        let (sketch_block_ref, _) = self
2399            .mutate_ast(
2400                &mut new_ast,
2401                sketch_id,
2402                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2403            )
2404            .map_err(KclErrorWithOutputs::no_outputs)?;
2405        // Convert to string source to create real source ranges.
2406        let new_source = source_from_ast(&new_ast);
2407        // Parse the new KCL source.
2408        let new_program = parse_frontend_mutation_source(
2409            &new_source,
2410            "Error parsing KCL source after adding arc",
2411            "No AST produced after adding arc",
2412        )?;
2413
2414        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2415            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2416                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2417            )))
2418        })?;
2419
2420        // Make sure to only set this if there are no errors.
2421        self.program = new_program.clone();
2422
2423        // Truncate after the sketch block for mock execution.
2424        let mut truncated_program = new_program;
2425        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2426            .map_err(KclErrorWithOutputs::no_outputs)?;
2427
2428        // Execute.
2429        let outcome = ctx
2430            .run_mock(
2431                &truncated_program,
2432                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2433            )
2434            .await?;
2435
2436        let new_object_ids = {
2437            let make_err =
2438                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2439            let segment_id = outcome
2440                .source_range_to_object
2441                .get(&arc_node_ref.range)
2442                .copied()
2443                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2444            let segment_object = outcome
2445                .scene_objects
2446                .get(segment_id.0)
2447                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2448            let ObjectKind::Segment { segment } = &segment_object.kind else {
2449                return Err(make_err(format!(
2450                    "Object is not a segment, it is {}",
2451                    segment_object.kind.human_friendly_kind_with_article()
2452                )));
2453            };
2454            let Segment::Arc(arc) = segment else {
2455                return Err(make_err(format!(
2456                    "Segment is not an arc, it is {}",
2457                    segment.human_friendly_kind_with_article()
2458                )));
2459            };
2460            vec![arc.start, arc.end, arc.center, segment_id]
2461        };
2462        let src_delta = SourceDelta { text: new_source };
2463        // Uses .no_freedom_analysis() so freedom_analysis: false
2464        let outcome = self.update_state_after_exec(outcome, false);
2465        let scene_graph_delta = SceneGraphDelta {
2466            new_graph: self.scene_graph_for_ui(),
2467            invalidates_ids: false,
2468            new_objects: new_object_ids,
2469            exec_outcome: outcome,
2470        };
2471        Ok((src_delta, scene_graph_delta))
2472    }
2473
2474    async fn add_circle(
2475        &mut self,
2476        ctx: &ExecutorContext,
2477        sketch: ObjectId,
2478        ctor: CircleCtor,
2479    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2480        // Create updated KCL source from args.
2481        let start_ast = to_ast_point2d(&ctor.start)
2482            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2483        let center_ast = to_ast_point2d(&ctor.center)
2484            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2485        let mut arguments = vec![
2486            ast::LabeledArg {
2487                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2488                arg: start_ast,
2489            },
2490            ast::LabeledArg {
2491                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2492                arg: center_ast,
2493            },
2494        ];
2495        // Add construction kwarg if construction is Some(true)
2496        if ctor.construction == Some(true) {
2497            arguments.push(ast::LabeledArg {
2498                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2499                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2500                    value: ast::LiteralValue::Bool(true),
2501                    raw: "true".to_string(),
2502                    digest: None,
2503                }))),
2504            });
2505        }
2506        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2507            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2508            unlabeled: None,
2509            arguments,
2510            digest: None,
2511            non_code_meta: Default::default(),
2512        })));
2513
2514        // Look up existing sketch.
2515        let sketch_id = sketch;
2516        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2517            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2518        })?;
2519        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2520            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2521                "Object is not a sketch, it is {}",
2522                sketch_object.kind.human_friendly_kind_with_article(),
2523            ))));
2524        };
2525        // Add the circle to the AST of the sketch block.
2526        let mut new_ast = self.program.ast.clone();
2527        let (sketch_block_ref, _) = self
2528            .mutate_ast(
2529                &mut new_ast,
2530                sketch_id,
2531                AstMutateCommand::AddSketchBlockVarDecl {
2532                    prefix: CIRCLE_VARIABLE.to_owned(),
2533                    expr: circle_ast,
2534                },
2535            )
2536            .map_err(KclErrorWithOutputs::no_outputs)?;
2537        // Convert to string source to create real source ranges.
2538        let new_source = source_from_ast(&new_ast);
2539        // Parse the new KCL source.
2540        let new_program = parse_frontend_mutation_source(
2541            &new_source,
2542            "Error parsing KCL source after adding circle",
2543            "No AST produced after adding circle",
2544        )?;
2545
2546        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2547            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2548                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2549            )))
2550        })?;
2551
2552        // Make sure to only set this if there are no errors.
2553        self.program = new_program.clone();
2554
2555        // Truncate after the sketch block for mock execution.
2556        let mut truncated_program = new_program;
2557        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2558            .map_err(KclErrorWithOutputs::no_outputs)?;
2559
2560        // Execute.
2561        let outcome = ctx
2562            .run_mock(
2563                &truncated_program,
2564                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2565            )
2566            .await?;
2567
2568        let new_object_ids = {
2569            let make_err =
2570                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2571            let segment_id = outcome
2572                .source_range_to_object
2573                .get(&circle_node_ref.range)
2574                .copied()
2575                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2576            let segment_object = outcome
2577                .scene_objects
2578                .get(segment_id.0)
2579                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2580            let ObjectKind::Segment { segment } = &segment_object.kind else {
2581                return Err(make_err(format!(
2582                    "Object is not a segment, it is {}",
2583                    segment_object.kind.human_friendly_kind_with_article()
2584                )));
2585            };
2586            let Segment::Circle(circle) = segment else {
2587                return Err(make_err(format!(
2588                    "Segment is not a circle, it is {}",
2589                    segment.human_friendly_kind_with_article()
2590                )));
2591            };
2592            vec![circle.start, circle.center, segment_id]
2593        };
2594        let src_delta = SourceDelta { text: new_source };
2595        // Uses .no_freedom_analysis() so freedom_analysis: false
2596        let outcome = self.update_state_after_exec(outcome, false);
2597        let scene_graph_delta = SceneGraphDelta {
2598            new_graph: self.scene_graph_for_ui(),
2599            invalidates_ids: false,
2600            new_objects: new_object_ids,
2601            exec_outcome: outcome,
2602        };
2603        Ok((src_delta, scene_graph_delta))
2604    }
2605
2606    async fn add_control_point_spline(
2607        &mut self,
2608        ctx: &ExecutorContext,
2609        sketch: ObjectId,
2610        ctor: ControlPointSplineCtor,
2611    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2612        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2613            .map_err(KclErrorWithOutputs::no_outputs)?;
2614
2615        let points_ast = to_ast_point2d_array(&ctor.points)
2616            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2617        let mut arguments = vec![ast::LabeledArg {
2618            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2619            arg: points_ast,
2620        }];
2621        if ctor.construction == Some(true) {
2622            arguments.push(ast::LabeledArg {
2623                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2624                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2625                    value: ast::LiteralValue::Bool(true),
2626                    raw: "true".to_string(),
2627                    digest: None,
2628                }))),
2629            });
2630        }
2631        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2632            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2633            unlabeled: None,
2634            arguments,
2635            digest: None,
2636            non_code_meta: Default::default(),
2637        })));
2638
2639        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2640            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2641        })?;
2642        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2643            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2644                "Object is not a sketch, it is {}",
2645                sketch_object.kind.human_friendly_kind_with_article(),
2646            ))));
2647        };
2648
2649        let mut new_ast = new_program.ast.clone();
2650        let (sketch_block_ref, _) = self
2651            .mutate_ast(
2652                &mut new_ast,
2653                sketch,
2654                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2655            )
2656            .map_err(KclErrorWithOutputs::no_outputs)?;
2657        let new_source = source_from_ast(&new_ast);
2658        let new_program = parse_frontend_mutation_source(
2659            &new_source,
2660            "Error parsing KCL source after adding controlPointSpline",
2661            "No AST produced after adding controlPointSpline",
2662        )?;
2663
2664        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2665            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2666                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2667            )))
2668        })?;
2669
2670        self.program = new_program.clone();
2671
2672        let mut truncated_program = new_program;
2673        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2674            .map_err(KclErrorWithOutputs::no_outputs)?;
2675
2676        let outcome = ctx
2677            .run_mock(
2678                &truncated_program,
2679                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2680            )
2681            .await?;
2682
2683        let new_object_ids = {
2684            let make_err =
2685                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2686            let segment_id = outcome
2687                .source_range_to_object
2688                .get(&spline_node_ref.range)
2689                .copied()
2690                .ok_or_else(|| {
2691                    make_err(format!(
2692                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2693                    ))
2694                })?;
2695            let segment_object = outcome
2696                .scene_objects
2697                .get(segment_id.0)
2698                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2699            let ObjectKind::Segment { segment } = &segment_object.kind else {
2700                return Err(make_err(format!(
2701                    "Object is not a segment, it is {}",
2702                    segment_object.kind.human_friendly_kind_with_article()
2703                )));
2704            };
2705            let Segment::ControlPointSpline(spline) = segment else {
2706                return Err(make_err(format!(
2707                    "Segment is not a control point spline, it is {}",
2708                    segment.human_friendly_kind_with_article()
2709                )));
2710            };
2711
2712            let mut ids = outcome
2713                .scene_objects
2714                .iter()
2715                .filter_map(|obj| match &obj.kind {
2716                    ObjectKind::Segment {
2717                        segment: Segment::Line(line),
2718                    } if line.owner == Some(segment_id) => Some(obj.id),
2719                    _ => None,
2720                })
2721                .collect::<Vec<_>>();
2722            ids.extend(spline.controls.clone());
2723            ids.push(segment_id);
2724            ids
2725        };
2726        let src_delta = SourceDelta { text: new_source };
2727        let outcome = self.update_state_after_exec(outcome, false);
2728        let scene_graph_delta = SceneGraphDelta {
2729            new_graph: self.scene_graph_for_ui(),
2730            invalidates_ids: false,
2731            new_objects: new_object_ids,
2732            exec_outcome: outcome,
2733        };
2734        Ok((src_delta, scene_graph_delta))
2735    }
2736
2737    fn edit_point(
2738        &mut self,
2739        new_ast: &mut ast::Node<ast::Program>,
2740        sketch: ObjectId,
2741        point: ObjectId,
2742        ctor: PointCtor,
2743    ) -> Result<(), KclError> {
2744        // Create updated KCL source from args.
2745        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2746
2747        // Look up existing sketch.
2748        let sketch_id = sketch;
2749        let sketch_object = self
2750            .scene_graph
2751            .objects
2752            .get(sketch_id.0)
2753            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2754        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2755            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2756        };
2757        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2758            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2759        })?;
2760        // Look up existing point.
2761        let point_id = point;
2762        let point_object = self
2763            .scene_graph
2764            .objects
2765            .get(point_id.0)
2766            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2767        let ObjectKind::Segment {
2768            segment: Segment::Point(point),
2769        } = &point_object.kind
2770        else {
2771            return Err(KclError::refactor(format!(
2772                "Object is not a point segment: {point_object:?}"
2773            )));
2774        };
2775
2776        // If the point is part of a line or arc, edit the line/arc instead.
2777        if let Some(owner_id) = point.owner {
2778            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2779                KclError::refactor(format!(
2780                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2781                ))
2782            })?;
2783            let ObjectKind::Segment { segment } = &owner_object.kind else {
2784                return Err(KclError::refactor(format!(
2785                    "Internal: Owner of point is not a segment, but found {}",
2786                    owner_object.kind.human_friendly_kind_with_article()
2787                )));
2788            };
2789
2790            // Handle Line owner
2791            if let Segment::Line(line) = segment {
2792                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2793                    return Err(KclError::refactor(format!(
2794                        "Internal: Owner of point does not have line ctor, but found {}",
2795                        line.ctor.human_friendly_kind_with_article()
2796                    )));
2797                };
2798                let mut line_ctor = line_ctor.clone();
2799                // Which end of the line is this point?
2800                if line.start == point_id {
2801                    line_ctor.start = ctor.position;
2802                } else if line.end == point_id {
2803                    line_ctor.end = ctor.position;
2804                } else {
2805                    return Err(KclError::refactor(format!(
2806                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2807                    )));
2808                }
2809                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2810            }
2811
2812            // Handle Arc owner
2813            if let Segment::Arc(arc) = segment {
2814                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2815                    return Err(KclError::refactor(format!(
2816                        "Internal: Owner of point does not have arc ctor, but found {}",
2817                        arc.ctor.human_friendly_kind_with_article()
2818                    )));
2819                };
2820                let mut arc_ctor = arc_ctor.clone();
2821                // Which point of the arc is this? (center, start, or end)
2822                if arc.center == point_id {
2823                    arc_ctor.center = ctor.position;
2824                } else if arc.start == point_id {
2825                    arc_ctor.start = ctor.position;
2826                } else if arc.end == point_id {
2827                    arc_ctor.end = ctor.position;
2828                } else {
2829                    return Err(KclError::refactor(format!(
2830                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2831                    )));
2832                }
2833                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2834            }
2835
2836            // Handle Circle owner
2837            if let Segment::Circle(circle) = segment {
2838                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2839                    return Err(KclError::refactor(format!(
2840                        "Internal: Owner of point does not have circle ctor, but found {}",
2841                        circle.ctor.human_friendly_kind_with_article()
2842                    )));
2843                };
2844                let mut circle_ctor = circle_ctor.clone();
2845                if circle.center == point_id {
2846                    circle_ctor.center = ctor.position;
2847                } else if circle.start == point_id {
2848                    circle_ctor.start = ctor.position;
2849                } else {
2850                    return Err(KclError::refactor(format!(
2851                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2852                    )));
2853                }
2854                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2855            }
2856
2857            if let Segment::ControlPointSpline(spline) = segment {
2858                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2859                    return Err(KclError::refactor(format!(
2860                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2861                        spline.ctor.human_friendly_kind_with_article()
2862                    )));
2863                };
2864                let mut spline_ctor = spline_ctor.clone();
2865                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2866                    return Err(KclError::refactor(format!(
2867                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2868                    )));
2869                };
2870                spline_ctor.points[control_index] = ctor.position;
2871                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2872            }
2873
2874            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2875            // (fall through to the point editing logic below)
2876        }
2877
2878        // Modify the point AST.
2879        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2880        Ok(())
2881    }
2882
2883    fn edit_line(
2884        &mut self,
2885        new_ast: &mut ast::Node<ast::Program>,
2886        sketch: ObjectId,
2887        line: ObjectId,
2888        ctor: LineCtor,
2889    ) -> Result<(), KclError> {
2890        // Create updated KCL source from args.
2891        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2892        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2893
2894        // Look up existing sketch.
2895        let sketch_id = sketch;
2896        let sketch_object = self
2897            .scene_graph
2898            .objects
2899            .get(sketch_id.0)
2900            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2901        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2902            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2903        };
2904        sketch
2905            .segments
2906            .iter()
2907            .find(|o| **o == line)
2908            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2909        // Look up existing line.
2910        let line_id = line;
2911        let line_object = self
2912            .scene_graph
2913            .objects
2914            .get(line_id.0)
2915            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2916        let ObjectKind::Segment { .. } = &line_object.kind else {
2917            let kind = line_object.kind.human_friendly_kind_with_article();
2918            return Err(KclError::refactor(format!(
2919                "This constraint only works on Segments, but you selected {kind}"
2920            )));
2921        };
2922
2923        // Modify the line AST.
2924        self.mutate_ast(
2925            new_ast,
2926            line_id,
2927            AstMutateCommand::EditLine {
2928                start: new_start_ast,
2929                end: new_end_ast,
2930                construction: ctor.construction,
2931            },
2932        )?;
2933        Ok(())
2934    }
2935
2936    fn edit_arc(
2937        &mut self,
2938        new_ast: &mut ast::Node<ast::Program>,
2939        sketch: ObjectId,
2940        arc: ObjectId,
2941        ctor: ArcCtor,
2942    ) -> Result<(), KclError> {
2943        // Create updated KCL source from args.
2944        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2945        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2946        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2947
2948        // Look up existing sketch.
2949        let sketch_id = sketch;
2950        let sketch_object = self
2951            .scene_graph
2952            .objects
2953            .get(sketch_id.0)
2954            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2955        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2956            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2957        };
2958        sketch
2959            .segments
2960            .iter()
2961            .find(|o| **o == arc)
2962            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2963        // Look up existing arc.
2964        let arc_id = arc;
2965        let arc_object = self
2966            .scene_graph
2967            .objects
2968            .get(arc_id.0)
2969            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2970        let ObjectKind::Segment { .. } = &arc_object.kind else {
2971            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2972        };
2973
2974        // Modify the arc AST.
2975        self.mutate_ast(
2976            new_ast,
2977            arc_id,
2978            AstMutateCommand::EditArc {
2979                start: new_start_ast,
2980                end: new_end_ast,
2981                center: new_center_ast,
2982                direction: ctor.direction,
2983                construction: ctor.construction,
2984            },
2985        )?;
2986        Ok(())
2987    }
2988
2989    fn edit_circle(
2990        &mut self,
2991        new_ast: &mut ast::Node<ast::Program>,
2992        sketch: ObjectId,
2993        circle: ObjectId,
2994        ctor: CircleCtor,
2995    ) -> Result<(), KclError> {
2996        // Create updated KCL source from args.
2997        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2998        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2999
3000        // Look up existing sketch.
3001        let sketch_id = sketch;
3002        let sketch_object = self
3003            .scene_graph
3004            .objects
3005            .get(sketch_id.0)
3006            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3007        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3008            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3009        };
3010        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3011            KclError::refactor(format!(
3012                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3013            ))
3014        })?;
3015        // Look up existing circle.
3016        let circle_id = circle;
3017        let circle_object = self
3018            .scene_graph
3019            .objects
3020            .get(circle_id.0)
3021            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3022        let ObjectKind::Segment { .. } = &circle_object.kind else {
3023            return Err(KclError::refactor(format!(
3024                "Object is not a segment: {circle_object:?}"
3025            )));
3026        };
3027
3028        // Modify the circle AST.
3029        self.mutate_ast(
3030            new_ast,
3031            circle_id,
3032            AstMutateCommand::EditCircle {
3033                start: new_start_ast,
3034                center: new_center_ast,
3035                construction: ctor.construction,
3036            },
3037        )?;
3038        Ok(())
3039    }
3040
3041    fn edit_control_point_spline(
3042        &mut self,
3043        new_ast: &mut ast::Node<ast::Program>,
3044        sketch: ObjectId,
3045        spline: ObjectId,
3046        ctor: ControlPointSplineCtor,
3047    ) -> Result<(), KclError> {
3048        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3049
3050        let sketch_object = self
3051            .scene_graph
3052            .objects
3053            .get(sketch.0)
3054            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3055        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3056            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3057        };
3058        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3059            KclError::refactor(format!(
3060                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3061            ))
3062        })?;
3063
3064        let spline_object =
3065            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3066                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3067            })?;
3068        let ObjectKind::Segment { .. } = &spline_object.kind else {
3069            return Err(KclError::refactor(format!(
3070                "Object is not a segment: {spline_object:?}"
3071            )));
3072        };
3073
3074        self.mutate_ast(
3075            new_ast,
3076            spline,
3077            AstMutateCommand::EditControlPointSpline {
3078                points: points_ast,
3079                construction: ctor.construction,
3080            },
3081        )?;
3082        Ok(())
3083    }
3084
3085    fn delete_segment(
3086        &mut self,
3087        new_ast: &mut ast::Node<ast::Program>,
3088        sketch: ObjectId,
3089        segment_id: ObjectId,
3090    ) -> Result<(), KclError> {
3091        // Look up existing sketch.
3092        let sketch_id = sketch;
3093        let sketch_object = self
3094            .scene_graph
3095            .objects
3096            .get(sketch_id.0)
3097            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3098        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3099            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3100        };
3101        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3102            KclError::refactor(format!(
3103                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3104            ))
3105        })?;
3106        // Look up existing segment.
3107        let segment_object =
3108            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3109                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3110            })?;
3111        let ObjectKind::Segment { .. } = &segment_object.kind else {
3112            return Err(KclError::refactor(format!(
3113                "Object is not a segment, it is {}",
3114                segment_object.kind.human_friendly_kind_with_article()
3115            )));
3116        };
3117
3118        // Modify the AST to remove the segment.
3119        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3120        Ok(())
3121    }
3122
3123    fn delete_constraint(
3124        &mut self,
3125        new_ast: &mut ast::Node<ast::Program>,
3126        sketch: ObjectId,
3127        constraint_id: ObjectId,
3128    ) -> Result<(), KclError> {
3129        // Look up existing sketch.
3130        let sketch_id = sketch;
3131        let sketch_object = self
3132            .scene_graph
3133            .objects
3134            .get(sketch_id.0)
3135            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3136        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3137            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3138        };
3139        sketch
3140            .constraints
3141            .iter()
3142            .find(|o| **o == constraint_id)
3143            .ok_or_else(|| {
3144                KclError::refactor(format!(
3145                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3146                ))
3147            })?;
3148        // Look up existing constraint.
3149        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3150            KclError::refactor(format!(
3151                "Constraint not found in scene graph: constraint={constraint_id:?}"
3152            ))
3153        })?;
3154        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3155            return Err(KclError::refactor(format!(
3156                "Object is not a constraint, it is {}",
3157                constraint_object.kind.human_friendly_kind_with_article()
3158            )));
3159        };
3160
3161        // Modify the AST to remove the constraint.
3162        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3163        Ok(())
3164    }
3165
3166    fn edit_coincident_constraint(
3167        &mut self,
3168        new_ast: &mut ast::Node<ast::Program>,
3169        constraint_id: ObjectId,
3170        segments: Vec<ConstraintSegment>,
3171    ) -> Result<(), KclError> {
3172        if segments.len() < 2 {
3173            return Err(KclError::refactor(format!(
3174                "Coincident constraint must have at least 2 inputs, got {}",
3175                segments.len()
3176            )));
3177        }
3178
3179        let segment_asts = segments
3180            .iter()
3181            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3182            .collect::<Result<Vec<_>, _>>()?;
3183
3184        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3185            elements: segment_asts,
3186            digest: None,
3187            non_code_meta: Default::default(),
3188        })));
3189
3190        self.mutate_ast(
3191            new_ast,
3192            constraint_id,
3193            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3194        )?;
3195        Ok(())
3196    }
3197
3198    fn edit_horizontal_points_constraint(
3199        &mut self,
3200        new_ast: &mut ast::Node<ast::Program>,
3201        constraint_id: ObjectId,
3202        points: Vec<ConstraintSegment>,
3203    ) -> Result<(), KclError> {
3204        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3205    }
3206
3207    fn edit_vertical_points_constraint(
3208        &mut self,
3209        new_ast: &mut ast::Node<ast::Program>,
3210        constraint_id: ObjectId,
3211        points: Vec<ConstraintSegment>,
3212    ) -> Result<(), KclError> {
3213        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3214    }
3215
3216    fn edit_axis_points_constraint(
3217        &mut self,
3218        new_ast: &mut ast::Node<ast::Program>,
3219        constraint_id: ObjectId,
3220        points: Vec<ConstraintSegment>,
3221        constraint_name: &str,
3222    ) -> Result<(), KclError> {
3223        if points.len() < 2 {
3224            return Err(KclError::refactor(format!(
3225                "{constraint_name} points constraint must have at least 2 points, got {}",
3226                points.len()
3227            )));
3228        }
3229
3230        let point_asts = points
3231            .iter()
3232            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3233            .collect::<Result<Vec<_>, _>>()?;
3234
3235        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3236            elements: point_asts,
3237            digest: None,
3238            non_code_meta: Default::default(),
3239        })));
3240
3241        self.mutate_ast(
3242            new_ast,
3243            constraint_id,
3244            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3245        )?;
3246        Ok(())
3247    }
3248
3249    /// updates the equalLength constraint with the given lines
3250    fn edit_equal_length_constraint(
3251        &mut self,
3252        new_ast: &mut ast::Node<ast::Program>,
3253        constraint_id: ObjectId,
3254        lines: Vec<ObjectId>,
3255    ) -> Result<(), KclError> {
3256        if lines.len() < 2 {
3257            return Err(KclError::refactor(format!(
3258                "Lines equal length constraint must have at least 2 lines, got {}",
3259                lines.len()
3260            )));
3261        }
3262
3263        let line_asts = lines
3264            .iter()
3265            .map(|line_id| {
3266                let line_object = self
3267                    .scene_graph
3268                    .objects
3269                    .get(line_id.0)
3270                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3271                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3272                    let kind = line_object.kind.human_friendly_kind_with_article();
3273                    return Err(KclError::refactor(format!(
3274                        "This constraint only works on Segments, but you selected {kind}"
3275                    )));
3276                };
3277                let Segment::Line(_) = line_segment else {
3278                    let kind = line_segment.human_friendly_kind_with_article();
3279                    return Err(KclError::refactor(format!(
3280                        "Only lines can be made equal length, but you selected {kind}"
3281                    )));
3282                };
3283
3284                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3285            })
3286            .collect::<Result<Vec<_>, _>>()?;
3287
3288        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3289            elements: line_asts,
3290            digest: None,
3291            non_code_meta: Default::default(),
3292        })));
3293
3294        self.mutate_ast(
3295            new_ast,
3296            constraint_id,
3297            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3298        )?;
3299        Ok(())
3300    }
3301
3302    /// Updates the parallel constraint with the given lines.
3303    fn edit_parallel_constraint(
3304        &mut self,
3305        new_ast: &mut ast::Node<ast::Program>,
3306        constraint_id: ObjectId,
3307        lines: Vec<ObjectId>,
3308    ) -> Result<(), KclError> {
3309        if lines.len() < 2 {
3310            return Err(KclError::refactor(format!(
3311                "Parallel constraint must have at least 2 lines, got {}",
3312                lines.len()
3313            )));
3314        }
3315
3316        let line_asts = lines
3317            .iter()
3318            .map(|line_id| {
3319                let line_object = self
3320                    .scene_graph
3321                    .objects
3322                    .get(line_id.0)
3323                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3324                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3325                    let kind = line_object.kind.human_friendly_kind_with_article();
3326                    return Err(KclError::refactor(format!(
3327                        "This constraint only works on Segments, but you selected {kind}"
3328                    )));
3329                };
3330                let Segment::Line(_) = line_segment else {
3331                    let kind = line_segment.human_friendly_kind_with_article();
3332                    return Err(KclError::refactor(format!(
3333                        "Only lines can be made parallel, but you selected {kind}"
3334                    )));
3335                };
3336
3337                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3338            })
3339            .collect::<Result<Vec<_>, _>>()?;
3340
3341        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3342            elements: line_asts,
3343            digest: None,
3344            non_code_meta: Default::default(),
3345        })));
3346
3347        self.mutate_ast(
3348            new_ast,
3349            constraint_id,
3350            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3351        )?;
3352        Ok(())
3353    }
3354
3355    /// Updates the equalRadius constraint with the given segments.
3356    fn edit_equal_radius_constraint(
3357        &mut self,
3358        new_ast: &mut ast::Node<ast::Program>,
3359        constraint_id: ObjectId,
3360        input: Vec<ObjectId>,
3361    ) -> Result<(), KclError> {
3362        if input.len() < 2 {
3363            return Err(KclError::refactor(format!(
3364                "equalRadius constraint must have at least 2 segments, got {}",
3365                input.len()
3366            )));
3367        }
3368
3369        let input_asts = input
3370            .iter()
3371            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3372            .collect::<Result<Vec<_>, _>>()?;
3373
3374        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3375            elements: input_asts,
3376            digest: None,
3377            non_code_meta: Default::default(),
3378        })));
3379
3380        self.mutate_ast(
3381            new_ast,
3382            constraint_id,
3383            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3384        )?;
3385        Ok(())
3386    }
3387
3388    async fn execute_after_edit(
3389        &mut self,
3390        ctx: &ExecutorContext,
3391        sketch: ObjectId,
3392        sketch_block_ref: AstNodeRef,
3393        new_ast: &mut ast::Node<ast::Program>,
3394        options: ExecuteAfterEditOptions,
3395    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3396        let ExecuteAfterEditOptions {
3397            segment_ids_edited,
3398            edit_kind,
3399            commit_solved_initial_guesses,
3400        } = options;
3401
3402        // Convert to string source to create real source ranges.
3403        let new_source = source_from_ast(new_ast);
3404        // Parse the new KCL source.
3405        let new_program = parse_frontend_mutation_source(
3406            &new_source,
3407            "Error parsing KCL source after editing",
3408            "No AST produced after editing",
3409        )?;
3410
3411        // Truncate after the sketch block for mock execution.
3412        let is_delete = edit_kind.is_delete();
3413        let truncated_program = {
3414            let mut truncated_program = new_program.clone();
3415            only_sketch_block(
3416                &mut truncated_program.ast,
3417                &sketch_block_ref,
3418                edit_kind.to_change_kind(),
3419            )
3420            .map_err(KclErrorWithOutputs::no_outputs)?;
3421            truncated_program
3422        };
3423
3424        // Execute.
3425        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3426        let mock_config = MockConfig {
3427            sketch_block_id: Some(sketch),
3428            freedom_analysis: is_delete,
3429            segment_ids_edited: segment_ids_edited.clone(),
3430            drag_anchors,
3431            ..Default::default()
3432        };
3433        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3434
3435        // Only now, after execution has succeeded, update self.program.
3436        self.program = new_program;
3437
3438        // Uses freedom_analysis: is_delete
3439        let outcome = self.update_state_after_exec(outcome, is_delete);
3440
3441        let src_delta = if commit_solved_initial_guesses {
3442            self.commit_var_solutions_to_program(&outcome, "editing")?
3443        } else {
3444            SourceDelta { text: new_source }
3445        };
3446        let scene_graph_delta = SceneGraphDelta {
3447            new_graph: self.scene_graph_for_ui(),
3448            invalidates_ids: is_delete,
3449            new_objects: Vec::new(),
3450            exec_outcome: outcome,
3451        };
3452        Ok((src_delta, scene_graph_delta))
3453    }
3454
3455    async fn execute_after_delete_sketch(
3456        &mut self,
3457        ctx: &ExecutorContext,
3458        new_ast: &mut ast::Node<ast::Program>,
3459    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3460        // Convert to string source to create real source ranges.
3461        let new_source = source_from_ast(new_ast);
3462        // Parse the new KCL source.
3463        let new_program = parse_frontend_mutation_source(
3464            &new_source,
3465            "Error parsing KCL source after editing",
3466            "No AST produced after editing",
3467        )?;
3468
3469        // Make sure to only set this if there are no errors.
3470        self.program = new_program.clone();
3471
3472        // We deleted the entire sketch block. It doesn't make sense to truncate
3473        // and execute only the sketch block. We execute the whole program with
3474        // a real engine.
3475
3476        // Execute.
3477        let outcome = ctx.run_with_caching(new_program).await?;
3478        let freedom_analysis_ran = true;
3479
3480        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3481
3482        let src_delta = SourceDelta { text: new_source };
3483        let scene_graph_delta = SceneGraphDelta {
3484            new_graph: self.scene_graph_for_ui(),
3485            invalidates_ids: true,
3486            new_objects: Vec::new(),
3487            exec_outcome: outcome,
3488        };
3489        Ok((src_delta, scene_graph_delta))
3490    }
3491
3492    /// Map a point object id into an AST reference expression for use in
3493    /// constraints. If the point is owned by a segment (line or arc), we
3494    /// reference the appropriate property on that segment (e.g. `line1.start`,
3495    /// `arc1.center`). Otherwise we reference the point directly.
3496    fn point_id_to_ast_reference(
3497        &self,
3498        point_id: ObjectId,
3499        new_ast: &mut ast::Node<ast::Program>,
3500    ) -> Result<ast::Expr, KclError> {
3501        let point_object = self
3502            .scene_graph
3503            .objects
3504            .get(point_id.0)
3505            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3506        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3507            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3508        };
3509        let Segment::Point(point) = point_segment else {
3510            return Err(KclError::refactor(format!(
3511                "Only points are currently supported: {point_object:?}"
3512            )));
3513        };
3514
3515        if let Some(owner_id) = point.owner {
3516            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3517                KclError::refactor(format!(
3518                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3519                ))
3520            })?;
3521            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3522                return Err(KclError::refactor(format!(
3523                    "Owner of point is not a segment, but found {}",
3524                    owner_object.kind.human_friendly_kind_with_article()
3525                )));
3526            };
3527
3528            match owner_segment {
3529                Segment::Line(line) => {
3530                    let property = if line.start == point_id {
3531                        LINE_PROPERTY_START
3532                    } else if line.end == point_id {
3533                        LINE_PROPERTY_END
3534                    } else {
3535                        return Err(KclError::refactor(format!(
3536                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3537                        )));
3538                    };
3539                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3540                }
3541                Segment::Arc(arc) => {
3542                    let property = if arc.start == point_id {
3543                        ARC_PROPERTY_START
3544                    } else if arc.end == point_id {
3545                        ARC_PROPERTY_END
3546                    } else if arc.center == point_id {
3547                        ARC_PROPERTY_CENTER
3548                    } else {
3549                        return Err(KclError::refactor(format!(
3550                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3551                        )));
3552                    };
3553                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3554                }
3555                Segment::Circle(circle) => {
3556                    let property = if circle.start == point_id {
3557                        CIRCLE_PROPERTY_START
3558                    } else if circle.center == point_id {
3559                        CIRCLE_PROPERTY_CENTER
3560                    } else {
3561                        return Err(KclError::refactor(format!(
3562                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3563                        )));
3564                    };
3565                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3566                }
3567                Segment::ControlPointSpline(spline) => {
3568                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3569                        return Err(KclError::refactor(format!(
3570                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3571                        )));
3572                    };
3573                    let owner_expr =
3574                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3575                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3576                    Ok(create_index_expression(controls_expr, index))
3577                }
3578                _ => Err(KclError::refactor(format!(
3579                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3580                ))),
3581            }
3582        } else {
3583            // Standalone point.
3584            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3585        }
3586    }
3587
3588    fn line_id_to_ast_reference(
3589        &self,
3590        line_id: ObjectId,
3591        new_ast: &mut ast::Node<ast::Program>,
3592    ) -> Result<ast::Expr, KclError> {
3593        let line_object = self
3594            .scene_graph
3595            .objects
3596            .get(line_id.0)
3597            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3598        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3599            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3600        };
3601        let Segment::Line(line) = line_segment else {
3602            return Err(KclError::refactor(format!(
3603                "Only lines are currently supported: {line_object:?}"
3604            )));
3605        };
3606
3607        if let Some(owner_id) = line.owner {
3608            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3609                KclError::refactor(format!(
3610                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3611                ))
3612            })?;
3613            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3614                return Err(KclError::refactor(format!(
3615                    "Owner of line is not a segment, but found {}",
3616                    owner_object.kind.human_friendly_kind_with_article()
3617                )));
3618            };
3619
3620            match owner_segment {
3621                Segment::ControlPointSpline(spline) => {
3622                    let Some(index) = spline
3623                        .controls
3624                        .windows(2)
3625                        .position(|window| window[0] == line.start && window[1] == line.end)
3626                    else {
3627                        return Err(KclError::refactor(format!(
3628                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3629                        )));
3630                    };
3631                    let owner_expr =
3632                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3633                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3634                    Ok(create_index_expression(edges_expr, index))
3635                }
3636                _ => Err(KclError::refactor(format!(
3637                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3638                ))),
3639            }
3640        } else {
3641            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3642        }
3643    }
3644
3645    fn coincident_segment_to_ast(
3646        &self,
3647        segment: &ConstraintSegment,
3648        new_ast: &mut ast::Node<ast::Program>,
3649    ) -> Result<ast::Expr, KclError> {
3650        match segment {
3651            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3652            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3653        }
3654    }
3655
3656    fn segment_id_to_constraint_ast_reference(
3657        &self,
3658        segment_id: ObjectId,
3659        new_ast: &mut ast::Node<ast::Program>,
3660    ) -> Result<ast::Expr, KclError> {
3661        let segment_object = self
3662            .scene_graph
3663            .objects
3664            .get(segment_id.0)
3665            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3666        let ObjectKind::Segment { segment } = &segment_object.kind else {
3667            return Err(KclError::refactor(format!(
3668                "Object is not a segment, it is {}",
3669                segment_object.kind.human_friendly_kind_with_article()
3670            )));
3671        };
3672
3673        match segment {
3674            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3675            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3676            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3677            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3678            Segment::ControlPointSpline(_) => {
3679                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3680            }
3681        }
3682    }
3683
3684    fn axis_constraint_segment_to_ast(
3685        &self,
3686        segment: &ConstraintSegment,
3687        new_ast: &mut ast::Node<ast::Program>,
3688    ) -> Result<ast::Expr, KclError> {
3689        match segment {
3690            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3691            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3692        }
3693    }
3694
3695    async fn add_coincident(
3696        &mut self,
3697        sketch: ObjectId,
3698        coincident: Coincident,
3699        new_ast: &mut ast::Node<ast::Program>,
3700    ) -> Result<AstNodeRef, KclError> {
3701        let sketch_id = sketch;
3702        for segment in &coincident.segments {
3703            let ConstraintSegment::Segment(segment_id) = segment else {
3704                continue;
3705            };
3706            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3707                continue;
3708            };
3709            if matches!(
3710                segment_object.kind,
3711                ObjectKind::Segment {
3712                    segment: Segment::ControlPointSpline(_)
3713                }
3714            ) {
3715                return Err(KclError::refactor(
3716                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3717                        .to_owned(),
3718                ));
3719            }
3720        }
3721        let segment_asts = coincident
3722            .segments
3723            .iter()
3724            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3725            .collect::<Result<Vec<_>, _>>()?;
3726        if segment_asts.len() < 2 {
3727            return Err(KclError::refactor(format!(
3728                "Coincident constraint must have at least 2 inputs, got {}",
3729                segment_asts.len()
3730            )));
3731        }
3732
3733        // Create the coincident() call using shared helper.
3734        let coincident_ast = create_coincident_ast(segment_asts);
3735
3736        // Add the line to the AST of the sketch block.
3737        let (sketch_block_ref, _) = self.mutate_ast(
3738            new_ast,
3739            sketch_id,
3740            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3741        )?;
3742        Ok(sketch_block_ref)
3743    }
3744
3745    async fn add_distance(
3746        &mut self,
3747        sketch: ObjectId,
3748        distance: Distance,
3749        new_ast: &mut ast::Node<ast::Program>,
3750    ) -> Result<AstNodeRef, KclError> {
3751        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3752    }
3753
3754    fn distance_constraint_ast_parts(
3755        &self,
3756        function_name: &str,
3757        distance: &Distance,
3758        new_ast: &mut ast::Node<ast::Program>,
3759    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3760        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3761            [pt0, pt1] => [
3762                self.coincident_segment_to_ast(pt0, new_ast)?,
3763                self.coincident_segment_to_ast(pt1, new_ast)?,
3764            ],
3765            _ => {
3766                return Err(KclError::refactor(format!(
3767                    "Distance constraint must have exactly 2 segments, got {}",
3768                    distance.segments.len()
3769                )));
3770            }
3771        };
3772
3773        let arguments = match &distance.label_position {
3774            Some(label_position) => vec![ast::LabeledArg {
3775                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3776                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3777            }],
3778            None => Default::default(),
3779        };
3780
3781        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3782            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3783            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3784                ast::ArrayExpression {
3785                    elements: vec![segment0_ast, segment1_ast],
3786                    digest: None,
3787                    non_code_meta: Default::default(),
3788                },
3789            )))),
3790            arguments,
3791            digest: None,
3792            non_code_meta: Default::default(),
3793        })));
3794        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3795            value: ast::LiteralValue::Number {
3796                value: distance.distance.value,
3797                suffix: distance.distance.units,
3798            },
3799            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3800                KclError::refactor(format!(
3801                    "Could not format numeric suffix: {:?}",
3802                    distance.distance.units
3803                ))
3804            })?,
3805            digest: None,
3806        })));
3807
3808        Ok((call, value))
3809    }
3810
3811    fn add_distance_constraint(
3812        &mut self,
3813        sketch: ObjectId,
3814        function_name: &str,
3815        distance: Distance,
3816        new_ast: &mut ast::Node<ast::Program>,
3817    ) -> Result<AstNodeRef, KclError> {
3818        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3819        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3820            left: call,
3821            operator: ast::BinaryOperator::Eq,
3822            right: value,
3823            digest: None,
3824        })));
3825
3826        let (sketch_block_ref, _) = self.mutate_ast(
3827            new_ast,
3828            sketch,
3829            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3830        )?;
3831        Ok(sketch_block_ref)
3832    }
3833
3834    async fn add_angle(
3835        &mut self,
3836        sketch: ObjectId,
3837        angle: Angle,
3838        new_ast: &mut ast::Node<ast::Program>,
3839    ) -> Result<AstNodeRef, KclError> {
3840        let sketch_id = sketch;
3841        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3842        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3843            left: angle_call_ast,
3844            operator: ast::BinaryOperator::Eq,
3845            right: angle_value_ast,
3846            digest: None,
3847        })));
3848
3849        // Add the line to the AST of the sketch block.
3850        let (sketch_block_ref, _) = self.mutate_ast(
3851            new_ast,
3852            sketch_id,
3853            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3854        )?;
3855        Ok(sketch_block_ref)
3856    }
3857
3858    fn angle_constraint_ast_parts(
3859        &self,
3860        angle: &Angle,
3861        new_ast: &mut ast::Node<ast::Program>,
3862    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3863        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3864            return Err(KclError::refactor(format!(
3865                "Angle constraint must have exactly 2 lines, got {}",
3866                angle.lines.len()
3867            )));
3868        };
3869
3870        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3871        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3872        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3873            elements: vec![l0_ast, l1_ast],
3874            digest: None,
3875            non_code_meta: Default::default(),
3876        })));
3877
3878        if angle.inverse == Some(true) && angle.sector.is_none() {
3879            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3880        }
3881
3882        let uses_angle_dimension = angle.sector.is_some();
3883        let mut arguments = if uses_angle_dimension {
3884            vec![ast::LabeledArg {
3885                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3886                arg: lines_ast.clone(),
3887            }]
3888        } else {
3889            Default::default()
3890        };
3891
3892        if let Some(sector) = angle.sector {
3893            arguments.push(ast::LabeledArg {
3894                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3895                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3896                    value: ast::LiteralValue::Number {
3897                        value: f64::from(sector),
3898                        suffix: NumericSuffix::None,
3899                    },
3900                    raw: sector.to_string(),
3901                    digest: None,
3902                }))),
3903            });
3904        }
3905
3906        if angle.inverse == Some(true) {
3907            arguments.push(ast::LabeledArg {
3908                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3909                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3910                    value: ast::LiteralValue::Bool(true),
3911                    raw: true.to_string(),
3912                    digest: None,
3913                }))),
3914            });
3915        }
3916
3917        if let Some(label_position) = &angle.label_position {
3918            arguments.push(ast::LabeledArg {
3919                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3920                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3921            });
3922        }
3923
3924        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3925            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3926                ANGLE_DIMENSION_FN
3927            } else {
3928                ANGLE_FN
3929            })),
3930            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3931            arguments,
3932            digest: None,
3933            non_code_meta: Default::default(),
3934        })));
3935        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3936            value: ast::LiteralValue::Number {
3937                value: angle.angle.value,
3938                suffix: angle.angle.units,
3939            },
3940            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3941                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3942            digest: None,
3943        })));
3944
3945        Ok((call, value))
3946    }
3947
3948    async fn add_tangent(
3949        &mut self,
3950        sketch: ObjectId,
3951        tangent: Tangent,
3952        new_ast: &mut ast::Node<ast::Program>,
3953    ) -> Result<AstNodeRef, KclError> {
3954        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3955            return Err(KclError::refactor(format!(
3956                "Tangent constraint must have exactly 2 segments, got {}",
3957                tangent.input.len()
3958            )));
3959        };
3960        let sketch_id = sketch;
3961
3962        let seg0_object = self
3963            .scene_graph
3964            .objects
3965            .get(seg0_id.0)
3966            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3967        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3968            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3969        };
3970        let seg0_ast = match seg0_segment {
3971            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3972                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3973            }
3974            _ => {
3975                return Err(KclError::refactor(format!(
3976                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3977                )));
3978            }
3979        };
3980
3981        let seg1_object = self
3982            .scene_graph
3983            .objects
3984            .get(seg1_id.0)
3985            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3986        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3987            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3988        };
3989        let seg1_ast = match seg1_segment {
3990            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3991                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3992            }
3993            _ => {
3994                return Err(KclError::refactor(format!(
3995                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3996                )));
3997            }
3998        };
3999
4000        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4001        let (sketch_block_ref, _) = self.mutate_ast(
4002            new_ast,
4003            sketch_id,
4004            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4005        )?;
4006        Ok(sketch_block_ref)
4007    }
4008
4009    async fn add_symmetric(
4010        &mut self,
4011        sketch: ObjectId,
4012        symmetric: Symmetric,
4013        new_ast: &mut ast::Node<ast::Program>,
4014    ) -> Result<AstNodeRef, KclError> {
4015        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4016            return Err(KclError::refactor(format!(
4017                "Symmetric constraint must have exactly 2 inputs, got {}",
4018                symmetric.input.len()
4019            )));
4020        };
4021        let sketch_id = sketch;
4022
4023        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4024        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4025        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4026
4027        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4028        let (sketch_block_ref, _) = self.mutate_ast(
4029            new_ast,
4030            sketch_id,
4031            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4032        )?;
4033        Ok(sketch_block_ref)
4034    }
4035
4036    async fn add_midpoint(
4037        &mut self,
4038        sketch: ObjectId,
4039        midpoint: Midpoint,
4040        new_ast: &mut ast::Node<ast::Program>,
4041    ) -> Result<AstNodeRef, KclError> {
4042        let sketch_id = sketch;
4043        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4044
4045        let segment_object = self
4046            .scene_graph
4047            .objects
4048            .get(midpoint.segment.0)
4049            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4050        let ObjectKind::Segment {
4051            segment: midpoint_segment,
4052        } = &segment_object.kind
4053        else {
4054            return Err(KclError::refactor(format!(
4055                "Object must be a segment, but it was {}",
4056                segment_object.kind.human_friendly_kind_with_article()
4057            )));
4058        };
4059        let segment_ast = match midpoint_segment {
4060            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4061            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4062            _ => {
4063                return Err(KclError::refactor(format!(
4064                    "Midpoint target must be a line or arc segment but it was {}",
4065                    midpoint_segment.human_friendly_kind_with_article()
4066                )));
4067            }
4068        };
4069
4070        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4071        let (sketch_block_ref, _) = self.mutate_ast(
4072            new_ast,
4073            sketch_id,
4074            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4075        )?;
4076        Ok(sketch_block_ref)
4077    }
4078
4079    async fn add_equal_radius(
4080        &mut self,
4081        sketch: ObjectId,
4082        equal_radius: EqualRadius,
4083        new_ast: &mut ast::Node<ast::Program>,
4084    ) -> Result<AstNodeRef, KclError> {
4085        if equal_radius.input.len() < 2 {
4086            return Err(KclError::refactor(format!(
4087                "equalRadius constraint must have at least 2 segments, got {}",
4088                equal_radius.input.len()
4089            )));
4090        }
4091
4092        let sketch_id = sketch;
4093        let input_asts = equal_radius
4094            .input
4095            .iter()
4096            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4097            .collect::<Result<Vec<_>, _>>()?;
4098
4099        let equal_radius_ast = create_equal_radius_ast(input_asts);
4100        let (sketch_block_ref, _) = self.mutate_ast(
4101            new_ast,
4102            sketch_id,
4103            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4104        )?;
4105        Ok(sketch_block_ref)
4106    }
4107
4108    async fn add_radius(
4109        &mut self,
4110        sketch: ObjectId,
4111        radius: Radius,
4112        new_ast: &mut ast::Node<ast::Program>,
4113    ) -> Result<AstNodeRef, KclError> {
4114        let params = ArcSizeConstraintParams {
4115            points: vec![radius.arc],
4116            function_name: RADIUS_FN,
4117            value: radius.radius.value,
4118            units: radius.radius.units,
4119            label_position: radius.label_position,
4120            constraint_type_name: "Radius",
4121        };
4122        self.add_arc_size_constraint(sketch, params, new_ast).await
4123    }
4124
4125    async fn add_diameter(
4126        &mut self,
4127        sketch: ObjectId,
4128        diameter: Diameter,
4129        new_ast: &mut ast::Node<ast::Program>,
4130    ) -> Result<AstNodeRef, KclError> {
4131        let params = ArcSizeConstraintParams {
4132            points: vec![diameter.arc],
4133            function_name: DIAMETER_FN,
4134            value: diameter.diameter.value,
4135            units: diameter.diameter.units,
4136            label_position: diameter.label_position,
4137            constraint_type_name: "Diameter",
4138        };
4139        self.add_arc_size_constraint(sketch, params, new_ast).await
4140    }
4141
4142    async fn add_fixed_constraints(
4143        &mut self,
4144        sketch: ObjectId,
4145        points: Vec<FixedPoint>,
4146        new_ast: &mut ast::Node<ast::Program>,
4147    ) -> Result<AstNodeRef, KclError> {
4148        let mut sketch_block_ref = None;
4149
4150        for fixed_point in points {
4151            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4152            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4153                .map_err(|err| KclError::refactor(err.to_string()))?;
4154
4155            let (sketch_ref, _) = self.mutate_ast(
4156                new_ast,
4157                sketch,
4158                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4159            )?;
4160            sketch_block_ref = Some(sketch_ref);
4161        }
4162
4163        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4164    }
4165
4166    async fn add_arc_size_constraint(
4167        &mut self,
4168        sketch: ObjectId,
4169        params: ArcSizeConstraintParams,
4170        new_ast: &mut ast::Node<ast::Program>,
4171    ) -> Result<AstNodeRef, KclError> {
4172        let sketch_id = sketch;
4173
4174        // Constraint must have exactly 1 argument (arc segment)
4175        if params.points.len() != 1 {
4176            return Err(KclError::refactor(format!(
4177                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4178                params.constraint_type_name,
4179                params.points.len()
4180            )));
4181        }
4182
4183        let arc_id = params.points[0];
4184        let arc_object = self
4185            .scene_graph
4186            .objects
4187            .get(arc_id.0)
4188            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4189        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4190            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4191        };
4192        let ref_type = match arc_segment {
4193            Segment::Arc(_) => ARC_VARIABLE,
4194            Segment::Circle(_) => CIRCLE_VARIABLE,
4195            _ => {
4196                return Err(KclError::refactor(format!(
4197                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4198                    params.constraint_type_name
4199                )));
4200            }
4201        };
4202        // Reference the arc/circle segment directly
4203        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4204        let arguments = match &params.label_position {
4205            Some(label_position) => vec![ast::LabeledArg {
4206                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4207                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4208            }],
4209            None => Default::default(),
4210        };
4211
4212        // Create the function call.
4213        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4214            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4215            unlabeled: Some(arc_ast),
4216            arguments,
4217            digest: None,
4218            non_code_meta: Default::default(),
4219        })));
4220        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4221            left: call_ast,
4222            operator: ast::BinaryOperator::Eq,
4223            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4224                value: ast::LiteralValue::Number {
4225                    value: params.value,
4226                    suffix: params.units,
4227                },
4228                raw: format_number_literal(params.value, params.units, None)
4229                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4230                digest: None,
4231            }))),
4232            digest: None,
4233        })));
4234
4235        // Add the line to the AST of the sketch block.
4236        let (sketch_block_ref, _) = self.mutate_ast(
4237            new_ast,
4238            sketch_id,
4239            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4240        )?;
4241        Ok(sketch_block_ref)
4242    }
4243
4244    async fn add_horizontal_distance(
4245        &mut self,
4246        sketch: ObjectId,
4247        distance: Distance,
4248        new_ast: &mut ast::Node<ast::Program>,
4249    ) -> Result<AstNodeRef, KclError> {
4250        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4251    }
4252
4253    async fn add_vertical_distance(
4254        &mut self,
4255        sketch: ObjectId,
4256        distance: Distance,
4257        new_ast: &mut ast::Node<ast::Program>,
4258    ) -> Result<AstNodeRef, KclError> {
4259        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4260    }
4261
4262    async fn add_horizontal(
4263        &mut self,
4264        sketch: ObjectId,
4265        horizontal: Horizontal,
4266        new_ast: &mut ast::Node<ast::Program>,
4267    ) -> Result<AstNodeRef, KclError> {
4268        let sketch_id = sketch;
4269
4270        // Map the runtime objects back to variable names.
4271        let first_arg_ast = match horizontal {
4272            Horizontal::Line { line } => {
4273                let line_object = self
4274                    .scene_graph
4275                    .objects
4276                    .get(line.0)
4277                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4278                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4279                    let kind = line_object.kind.human_friendly_kind_with_article();
4280                    return Err(KclError::refactor(format!(
4281                        "This constraint only works on Segments, but you selected {kind}"
4282                    )));
4283                };
4284                let Segment::Line(_) = line_segment else {
4285                    return Err(KclError::refactor(format!(
4286                        "Only lines can be made horizontal, but you selected {}",
4287                        line_segment.human_friendly_kind_with_article(),
4288                    )));
4289                };
4290                self.line_id_to_ast_reference(line, new_ast)?
4291            }
4292            Horizontal::Points { points } => {
4293                let point_asts = points
4294                    .iter()
4295                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4296                    .collect::<Result<Vec<_>, _>>()?;
4297                ast::ArrayExpression::new(point_asts).into()
4298            }
4299        };
4300        // Create the horizontal() call using shared helper.
4301        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4302
4303        // Add the line to the AST of the sketch block.
4304        let (sketch_block_ref, _) = self.mutate_ast(
4305            new_ast,
4306            sketch_id,
4307            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4308        )?;
4309        Ok(sketch_block_ref)
4310    }
4311
4312    async fn add_lines_equal_length(
4313        &mut self,
4314        sketch: ObjectId,
4315        lines_equal_length: LinesEqualLength,
4316        new_ast: &mut ast::Node<ast::Program>,
4317    ) -> Result<AstNodeRef, KclError> {
4318        if lines_equal_length.lines.len() < 2 {
4319            return Err(KclError::refactor(format!(
4320                "Lines equal length constraint must have at least 2 lines, got {}",
4321                lines_equal_length.lines.len()
4322            )));
4323        };
4324
4325        let sketch_id = sketch;
4326
4327        // Map the runtime objects back to variable names.
4328        let line_asts = lines_equal_length
4329            .lines
4330            .iter()
4331            .map(|line_id| {
4332                let line_object = self
4333                    .scene_graph
4334                    .objects
4335                    .get(line_id.0)
4336                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4337                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4338                    let kind = line_object.kind.human_friendly_kind_with_article();
4339                    return Err(KclError::refactor(format!(
4340                        "This constraint only works on Segments, but you selected {kind}"
4341                    )));
4342                };
4343                let Segment::Line(_) = line_segment else {
4344                    let kind = line_segment.human_friendly_kind_with_article();
4345                    return Err(KclError::refactor(format!(
4346                        "Only lines can be made equal length, but you selected {kind}"
4347                    )));
4348                };
4349
4350                self.line_id_to_ast_reference(*line_id, new_ast)
4351            })
4352            .collect::<Result<Vec<_>, _>>()?;
4353
4354        // Create the equalLength() call using shared helper.
4355        let equal_length_ast = create_equal_length_ast(line_asts);
4356
4357        // Add the constraint to the AST of the sketch block.
4358        let (sketch_block_ref, _) = self.mutate_ast(
4359            new_ast,
4360            sketch_id,
4361            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4362        )?;
4363        Ok(sketch_block_ref)
4364    }
4365
4366    fn equal_radius_segment_id_to_ast_reference(
4367        &mut self,
4368        segment_id: ObjectId,
4369        new_ast: &mut ast::Node<ast::Program>,
4370    ) -> Result<ast::Expr, KclError> {
4371        let segment_object = self
4372            .scene_graph
4373            .objects
4374            .get(segment_id.0)
4375            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4376        let ObjectKind::Segment { segment } = &segment_object.kind else {
4377            return Err(KclError::refactor(format!(
4378                "Object is not a segment, it was {}",
4379                segment_object.kind.human_friendly_kind_with_article()
4380            )));
4381        };
4382
4383        let ref_type = match segment {
4384            Segment::Arc(_) => ARC_VARIABLE,
4385            Segment::Circle(_) => CIRCLE_VARIABLE,
4386            _ => {
4387                return Err(KclError::refactor(format!(
4388                    "equalRadius supports only arc/circle segments, got {}",
4389                    segment.human_friendly_kind_with_article()
4390                )));
4391            }
4392        };
4393
4394        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4395    }
4396
4397    fn symmetric_input_id_to_ast_reference(
4398        &mut self,
4399        segment_id: ObjectId,
4400        new_ast: &mut ast::Node<ast::Program>,
4401    ) -> Result<ast::Expr, KclError> {
4402        let segment_object = self
4403            .scene_graph
4404            .objects
4405            .get(segment_id.0)
4406            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4407        let ObjectKind::Segment { segment } = &segment_object.kind else {
4408            return Err(KclError::refactor(format!(
4409                "Object is not a segment, it was {}",
4410                segment_object.kind.human_friendly_kind_with_article()
4411            )));
4412        };
4413
4414        match segment {
4415            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4416            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4417            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4418            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4419            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4420                "Symmetric does not yet support control point splines".to_owned(),
4421            )),
4422        }
4423    }
4424
4425    fn symmetric_axis_id_to_ast_reference(
4426        &mut self,
4427        segment_id: ObjectId,
4428        new_ast: &mut ast::Node<ast::Program>,
4429    ) -> Result<ast::Expr, KclError> {
4430        let segment_object = self
4431            .scene_graph
4432            .objects
4433            .get(segment_id.0)
4434            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4435        let ObjectKind::Segment { segment } = &segment_object.kind else {
4436            return Err(KclError::refactor(format!(
4437                "Object is not a segment, it was {}",
4438                segment_object.kind.human_friendly_kind_with_article()
4439            )));
4440        };
4441        match segment {
4442            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4443            _ => Err(KclError::refactor(format!(
4444                "Symmetric axis must be a line, got {}",
4445                segment.human_friendly_kind_with_article()
4446            ))),
4447        }
4448    }
4449
4450    async fn add_parallel(
4451        &mut self,
4452        sketch: ObjectId,
4453        parallel: Parallel,
4454        new_ast: &mut ast::Node<ast::Program>,
4455    ) -> Result<AstNodeRef, KclError> {
4456        if parallel.lines.len() < 2 {
4457            return Err(KclError::refactor(format!(
4458                "Parallel constraint must have at least 2 lines, got {}",
4459                parallel.lines.len()
4460            )));
4461        };
4462
4463        let sketch_id = sketch;
4464
4465        let line_asts = parallel
4466            .lines
4467            .iter()
4468            .map(|line_id| {
4469                let line_object = self
4470                    .scene_graph
4471                    .objects
4472                    .get(line_id.0)
4473                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4474                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4475                    let kind = line_object.kind.human_friendly_kind_with_article();
4476                    return Err(KclError::refactor(format!(
4477                        "This constraint only works on Segments, but you selected {kind}"
4478                    )));
4479                };
4480                let Segment::Line(_) = line_segment else {
4481                    let kind = line_segment.human_friendly_kind_with_article();
4482                    return Err(KclError::refactor(format!(
4483                        "Only lines can be made parallel, but you selected {kind}"
4484                    )));
4485                };
4486
4487                self.line_id_to_ast_reference(*line_id, new_ast)
4488            })
4489            .collect::<Result<Vec<_>, _>>()?;
4490
4491        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4492            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4493            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4494                ast::ArrayExpression {
4495                    elements: line_asts,
4496                    digest: None,
4497                    non_code_meta: Default::default(),
4498                },
4499            )))),
4500            arguments: Default::default(),
4501            digest: None,
4502            non_code_meta: Default::default(),
4503        })));
4504
4505        let (sketch_block_ref, _) = self.mutate_ast(
4506            new_ast,
4507            sketch_id,
4508            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4509        )?;
4510        Ok(sketch_block_ref)
4511    }
4512
4513    async fn add_perpendicular(
4514        &mut self,
4515        sketch: ObjectId,
4516        perpendicular: Perpendicular,
4517        new_ast: &mut ast::Node<ast::Program>,
4518    ) -> Result<AstNodeRef, KclError> {
4519        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4520            .await
4521    }
4522
4523    async fn add_lines_at_angle_constraint(
4524        &mut self,
4525        sketch: ObjectId,
4526        angle_kind: LinesAtAngleKind,
4527        lines: Vec<ObjectId>,
4528        new_ast: &mut ast::Node<ast::Program>,
4529    ) -> Result<AstNodeRef, KclError> {
4530        let &[line0_id, line1_id] = lines.as_slice() else {
4531            return Err(KclError::refactor(format!(
4532                "{} constraint must have exactly 2 lines, got {}",
4533                angle_kind.to_function_name(),
4534                lines.len()
4535            )));
4536        };
4537
4538        let sketch_id = sketch;
4539
4540        // Map the runtime objects back to variable names.
4541        let line0_object = self
4542            .scene_graph
4543            .objects
4544            .get(line0_id.0)
4545            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4546        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4547            let kind = line0_object.kind.human_friendly_kind_with_article();
4548            return Err(KclError::refactor(format!(
4549                "This constraint only works on Segments, but you selected {kind}"
4550            )));
4551        };
4552        let Segment::Line(_) = line0_segment else {
4553            return Err(KclError::refactor(format!(
4554                "Only lines can be made {}, but you selected {}",
4555                angle_kind.to_function_name(),
4556                line0_segment.human_friendly_kind_with_article(),
4557            )));
4558        };
4559        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4560
4561        let line1_object = self
4562            .scene_graph
4563            .objects
4564            .get(line1_id.0)
4565            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4566        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4567            let kind = line1_object.kind.human_friendly_kind_with_article();
4568            return Err(KclError::refactor(format!(
4569                "This constraint only works on Segments, but you selected {kind}"
4570            )));
4571        };
4572        let Segment::Line(_) = line1_segment else {
4573            return Err(KclError::refactor(format!(
4574                "Only lines can be made {}, but you selected {}",
4575                angle_kind.to_function_name(),
4576                line1_segment.human_friendly_kind_with_article(),
4577            )));
4578        };
4579        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4580
4581        // Create the parallel() or perpendicular() call.
4582        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4583            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4584            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4585                ast::ArrayExpression {
4586                    elements: vec![line0_ast, line1_ast],
4587                    digest: None,
4588                    non_code_meta: Default::default(),
4589                },
4590            )))),
4591            arguments: Default::default(),
4592            digest: None,
4593            non_code_meta: Default::default(),
4594        })));
4595
4596        // Add the constraint to the AST of the sketch block.
4597        let (sketch_block_ref, _) = self.mutate_ast(
4598            new_ast,
4599            sketch_id,
4600            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4601        )?;
4602        Ok(sketch_block_ref)
4603    }
4604
4605    async fn add_vertical(
4606        &mut self,
4607        sketch: ObjectId,
4608        vertical: Vertical,
4609        new_ast: &mut ast::Node<ast::Program>,
4610    ) -> Result<AstNodeRef, KclError> {
4611        let sketch_id = sketch;
4612
4613        let first_arg_ast = match vertical {
4614            Vertical::Line { line } => {
4615                // Map the runtime objects back to variable names.
4616                let line_object = self
4617                    .scene_graph
4618                    .objects
4619                    .get(line.0)
4620                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4621                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4622                    let kind = line_object.kind.human_friendly_kind_with_article();
4623                    return Err(KclError::refactor(format!(
4624                        "This constraint only works on Segments, but you selected {kind}"
4625                    )));
4626                };
4627                let Segment::Line(_) = line_segment else {
4628                    return Err(KclError::refactor(format!(
4629                        "Only lines can be made vertical, but you selected {}",
4630                        line_segment.human_friendly_kind_with_article()
4631                    )));
4632                };
4633                self.line_id_to_ast_reference(line, new_ast)?
4634            }
4635            Vertical::Points { points } => {
4636                let point_asts = points
4637                    .iter()
4638                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4639                    .collect::<Result<Vec<_>, _>>()?;
4640                ast::ArrayExpression::new(point_asts).into()
4641            }
4642        };
4643        // Create the vertical() call using shared helper.
4644        let vertical_ast = create_vertical_ast(first_arg_ast);
4645
4646        // Add the line to the AST of the sketch block.
4647        let (sketch_block_ref, _) = self.mutate_ast(
4648            new_ast,
4649            sketch_id,
4650            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4651        )?;
4652        Ok(sketch_block_ref)
4653    }
4654
4655    async fn execute_after_add_constraint(
4656        &mut self,
4657        ctx: &ExecutorContext,
4658        sketch_id: ObjectId,
4659        sketch_block_ref: AstNodeRef,
4660        new_ast: &mut ast::Node<ast::Program>,
4661    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4662        // Convert to string source to create real source ranges.
4663        let new_source = source_from_ast(new_ast);
4664        // Parse the new KCL source.
4665        let new_program = parse_frontend_mutation_source(
4666            &new_source,
4667            "Error parsing KCL source after adding constraint",
4668            "No AST produced after adding constraint",
4669        )?;
4670        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4671            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4672                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4673            )))
4674        })?;
4675
4676        // Truncate after the sketch block for mock execution.
4677        // Use a clone so we don't mutate new_program yet
4678        let mut truncated_program = new_program.clone();
4679        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4680            .map_err(KclErrorWithOutputs::no_outputs)?;
4681
4682        // Execute - if this fails, we haven't modified self yet, so state is safe
4683        let outcome = ctx
4684            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4685            .await?;
4686
4687        let new_object_ids = {
4688            // Extract the constraint ID from the execution outcome using source_range_to_object
4689            let constraint_id = outcome
4690                .source_range_to_object
4691                .get(&constraint_node_ref.range)
4692                .copied()
4693                .ok_or_else(|| {
4694                    KclErrorWithOutputs::from_error_outcome(
4695                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4696                        outcome.clone(),
4697                    )
4698                })?;
4699            vec![constraint_id]
4700        };
4701
4702        // Only now, after all operations succeeded, update self.program.
4703        // This ensures state is only modified if everything succeeds.
4704        self.program = new_program;
4705
4706        // Uses MockConfig::default() which has freedom_analysis: true
4707        let outcome = self.update_state_after_exec(outcome, true);
4708
4709        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4710        let scene_graph_delta = SceneGraphDelta {
4711            new_graph: self.scene_graph_for_ui(),
4712            invalidates_ids: false,
4713            new_objects: new_object_ids,
4714            exec_outcome: outcome,
4715        };
4716        Ok((src_delta, scene_graph_delta))
4717    }
4718
4719    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4720        let commit_failure = || {
4721            KclErrorWithOutputs::from_error_outcome(
4722                KclError::refactor(format!("Could not update KCL after {operation}.")),
4723                outcome.clone(),
4724            )
4725        };
4726
4727        let default_length_unit = self.default_length_unit();
4728        let mut settled_ast = self.program.ast.clone();
4729        let mut committed_solver_value = false;
4730        for (var_range, node_path, value) in &outcome.var_solutions {
4731            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4732                return Err(commit_failure());
4733            };
4734            let new_value = match &lookup {
4735                Some(current_literal) => {
4736                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4737                        continue;
4738                    }
4739                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4740                }
4741                None => {
4742                    // Bare `var` with no initial literal to compare against;
4743                    // always commit, using the module's default length unit as
4744                    // an explicit suffix so the written value carries units.
4745                    Number {
4746                        value: number_value_in_default_length_units(*value, default_length_unit),
4747                        units: default_length_unit.into(),
4748                    }
4749                }
4750            };
4751            committed_solver_value = true;
4752            let source_ref = SourceRef::Simple {
4753                range: *var_range,
4754                node_path: node_path.clone(),
4755            };
4756            mutate_ast_node_by_source_ref(
4757                &mut settled_ast,
4758                &source_ref,
4759                AstMutateCommand::EditVarInitialValue { value: new_value },
4760            )
4761            .map_err(|_| commit_failure())?;
4762        }
4763
4764        if !committed_solver_value {
4765            return Ok(SourceDelta {
4766                text: self.program.original_file_contents.clone(),
4767            });
4768        }
4769
4770        let settled_source = source_from_ast(&settled_ast);
4771        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4772        if !errors.is_empty() {
4773            return Err(commit_failure());
4774        }
4775        let Some(settled_program) = settled_program else {
4776            return Err(commit_failure());
4777        };
4778
4779        self.program = settled_program;
4780
4781        Ok(SourceDelta { text: settled_source })
4782    }
4783
4784    // Find constraints that reference the given segments.
4785    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4786        if segment_ids_set.contains(&segment_id) {
4787            return true;
4788        }
4789
4790        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4791            return false;
4792        };
4793        let ObjectKind::Segment { segment } = &segment_object.kind else {
4794            return false;
4795        };
4796        let Segment::Point(point) = segment else {
4797            return false;
4798        };
4799
4800        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4801    }
4802
4803    fn remaining_constraint_segments(
4804        &self,
4805        segments: &[ConstraintSegment],
4806        segment_ids_set: &AhashIndexSet<ObjectId>,
4807    ) -> Vec<ConstraintSegment> {
4808        segments
4809            .iter()
4810            .copied()
4811            .filter(|segment| match segment {
4812                ConstraintSegment::Origin(_) => true,
4813                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4814            })
4815            .collect()
4816    }
4817
4818    fn find_referenced_constraints(
4819        &self,
4820        sketch_id: ObjectId,
4821        segment_ids_set: &AhashIndexSet<ObjectId>,
4822    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4823        // Look up the sketch.
4824        let sketch_object = self
4825            .scene_graph
4826            .objects
4827            .get(sketch_id.0)
4828            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4829        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4830            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4831        };
4832        let segment_or_owner_matches = |segment_id: ObjectId| {
4833            if segment_ids_set.contains(&segment_id) {
4834                return true;
4835            }
4836            let segment_object = self.scene_graph.objects.get(segment_id.0);
4837            if let Some(obj) = segment_object
4838                && let ObjectKind::Segment { segment } = &obj.kind
4839            {
4840                match segment {
4841                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4842                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4843                    _ => false,
4844                }
4845            } else {
4846                false
4847            }
4848        };
4849        let mut constraint_ids_set = AhashIndexSet::default();
4850        for constraint_id in &sketch.constraints {
4851            let constraint_object = self
4852                .scene_graph
4853                .objects
4854                .get(constraint_id.0)
4855                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4856            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4857                return Err(KclError::refactor(format!(
4858                    "Object is not a constraint, it is {}",
4859                    constraint_object.kind.human_friendly_kind_with_article()
4860                )));
4861            };
4862            let depends_on_segment = match constraint {
4863                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4864                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4865                Constraint::Fixed(fixed) => fixed
4866                    .points
4867                    .iter()
4868                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4869                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4870                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4871                Constraint::EqualRadius(equal_radius) => {
4872                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4873                }
4874                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4875                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4876                Constraint::Horizontal(h) => match h {
4877                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4878                    Horizontal::Points { points } => points.iter().any(|point| match point {
4879                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4880                        ConstraintSegment::Origin(_) => false,
4881                    }),
4882                },
4883                Constraint::Vertical(v) => match v {
4884                    Vertical::Line { line } => segment_or_owner_matches(*line),
4885                    Vertical::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::LinesEqualLength(lines_equal_length) => {
4891                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4892                }
4893                Constraint::Midpoint(midpoint) => {
4894                    segment_or_owner_matches(midpoint.segment)
4895                        || matches!(
4896                            midpoint.point,
4897                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4898                        )
4899                }
4900                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4901                Constraint::Perpendicular(perpendicular) => {
4902                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4903                }
4904                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4905                Constraint::Symmetric(symmetric) => {
4906                    segment_or_owner_matches(symmetric.axis)
4907                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4908                }
4909                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4910            };
4911            if depends_on_segment {
4912                constraint_ids_set.insert(*constraint_id);
4913            }
4914        }
4915        Ok(constraint_ids_set)
4916    }
4917
4918    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4919        let mut outcome = outcome;
4920        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4921        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4922
4923        if freedom_analysis_ran {
4924            // When freedom analysis ran, replace the cache entirely with new values
4925            // Don't merge with old values since IDs might have changed
4926            self.point_freedom_cache.clear();
4927            for new_obj in &new_objects {
4928                if let ObjectKind::Segment {
4929                    segment: crate::front::Segment::Point(point),
4930                } = &new_obj.kind
4931                {
4932                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4933                }
4934            }
4935            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4936            // Objects are already correct from the analysis, just use them as-is
4937            self.scene_graph.objects = new_objects;
4938        } else {
4939            // When freedom analysis didn't run, preserve old values and merge
4940            // Before replacing objects, extract and store freedom values from old objects
4941            for old_obj in &self.scene_graph.objects {
4942                if let ObjectKind::Segment {
4943                    segment: crate::front::Segment::Point(point),
4944                } = &old_obj.kind
4945                {
4946                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4947                }
4948            }
4949
4950            // Update objects, preserving stored freedom values when new is Free (might be default)
4951            let mut updated_objects = Vec::with_capacity(new_objects.len());
4952            for new_obj in new_objects {
4953                let mut obj = new_obj;
4954                if let ObjectKind::Segment {
4955                    segment: crate::front::Segment::Point(point),
4956                } = &mut obj.kind
4957                {
4958                    let new_freedom = point.freedom;
4959                    // When freedom_analysis=false, new values are defaults (Free).
4960                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4961                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4962                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4963                    // when there are actually unsatisfied constraints.
4964                    match new_freedom {
4965                        Freedom::Free => {
4966                            match self.point_freedom_cache.get(&obj.id).copied() {
4967                                Some(Freedom::Conflict) => {
4968                                    // Don't preserve Conflict - conflicts are transient
4969                                    // Keep it as Free
4970                                }
4971                                Some(Freedom::Fixed) => {
4972                                    // Preserve Fixed cached value
4973                                    point.freedom = Freedom::Fixed;
4974                                }
4975                                Some(Freedom::Free) => {
4976                                    // If stored is also Free, keep Free (no change needed)
4977                                }
4978                                None => {
4979                                    // If no cached value, keep Free (default)
4980                                }
4981                            }
4982                        }
4983                        Freedom::Fixed => {
4984                            // Use new value (already set)
4985                        }
4986                        Freedom::Conflict => {
4987                            // Use new value (already set)
4988                        }
4989                    }
4990                    // Store the new freedom value (even if it's Free, so we know it was set)
4991                    self.point_freedom_cache.insert(obj.id, point.freedom);
4992                }
4993                updated_objects.push(obj);
4994            }
4995
4996            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4997            self.scene_graph.objects = updated_objects;
4998        }
4999        outcome
5000    }
5001
5002    fn mutate_ast(
5003        &mut self,
5004        ast: &mut ast::Node<ast::Program>,
5005        object_id: ObjectId,
5006        command: AstMutateCommand,
5007    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5008        let sketch_object = self
5009            .scene_graph
5010            .objects
5011            .get(object_id.0)
5012            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5013        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5014    }
5015
5016    fn mutate_constraint_label_position(
5017        &mut self,
5018        ast: &mut ast::Node<ast::Program>,
5019        constraint_id: ObjectId,
5020        label_position: Point2d<Number>,
5021    ) -> Result<(), KclError> {
5022        let object = self
5023            .scene_graph
5024            .objects
5025            .get(constraint_id.0)
5026            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5027        if !matches!(
5028            &object.kind,
5029            ObjectKind::Constraint {
5030                constraint: Constraint::Distance(_)
5031                    | Constraint::HorizontalDistance(_)
5032                    | Constraint::VerticalDistance(_)
5033                    | Constraint::Radius(_)
5034                    | Constraint::Diameter(_)
5035                    | Constraint::Angle(_),
5036            }
5037        ) {
5038            return Err(KclError::refactor(format!(
5039                "Object does not support labelPosition: {constraint_id:?}"
5040            )));
5041        }
5042
5043        let label_position = to_ast_point2d_number(&label_position)
5044            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5045        self.mutate_ast(
5046            ast,
5047            constraint_id,
5048            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5049        )?;
5050        Ok(())
5051    }
5052}
5053
5054fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5055    // Look up existing sketch.
5056    let sketch_object = scene_graph
5057        .objects
5058        .get(sketch_id.0)
5059        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5060    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5061        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5062    };
5063    expect_single_node_ref(sketch_object)
5064}
5065
5066fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5067    match &object.source {
5068        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5069            range: *range,
5070            node_path: node_path.clone(),
5071        }),
5072        SourceRef::BackTrace { ranges } => {
5073            let [range] = ranges.as_slice() else {
5074                return Err(KclError::refactor(format!(
5075                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5076                    ranges.len()
5077                )));
5078            };
5079            Ok(AstNodeRef {
5080                range: range.0,
5081                node_path: range.1.clone(),
5082            })
5083        }
5084    }
5085}
5086
5087/// This is a deprecated fall-back implementation. Prefer
5088/// [`only_sketch_block()`] to avoid reliance on source ranges.
5089fn only_sketch_block_from_range(
5090    ast: &mut ast::Node<ast::Program>,
5091    sketch_block_range: SourceRange,
5092    edit_kind: ChangeKind,
5093) -> Result<(), KclError> {
5094    let r1 = sketch_block_range;
5095    let matches_range = |r2: SourceRange| -> bool {
5096        // We may have added items to the sketch block, so the end may not be an
5097        // exact match.
5098        match edit_kind {
5099            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5100            // For edit, we don't know whether it grew or shrank.
5101            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5102            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5103            // No edit should be an exact match.
5104            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5105        }
5106    };
5107    let mut found = false;
5108    for item in ast.body.iter_mut() {
5109        match item {
5110            ast::BodyItem::ImportStatement(_) => {}
5111            ast::BodyItem::ExpressionStatement(node) => {
5112                if matches_range(SourceRange::from(&*node))
5113                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5114                {
5115                    sketch_block.is_being_edited = true;
5116                    found = true;
5117                    break;
5118                }
5119            }
5120            ast::BodyItem::VariableDeclaration(node) => {
5121                if matches_range(SourceRange::from(&node.declaration.init))
5122                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5123                {
5124                    sketch_block.is_being_edited = true;
5125                    found = true;
5126                    break;
5127                }
5128            }
5129            ast::BodyItem::TypeDeclaration(_) => {}
5130            ast::BodyItem::ReturnStatement(node) => {
5131                if matches_range(SourceRange::from(&node.argument))
5132                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5133                {
5134                    sketch_block.is_being_edited = true;
5135                    found = true;
5136                    break;
5137                }
5138            }
5139        }
5140    }
5141    if !found {
5142        return Err(KclError::refactor(format!(
5143            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5144        )));
5145    }
5146
5147    Ok(())
5148}
5149
5150fn only_sketch_block(
5151    ast: &mut ast::Node<ast::Program>,
5152    sketch_block_ref: &AstNodeRef,
5153    edit_kind: ChangeKind,
5154) -> Result<(), KclError> {
5155    let Some(target_node_path) = &sketch_block_ref.node_path else {
5156        #[cfg(target_arch = "wasm32")]
5157        web_sys::console::warn_1(
5158            &format!(
5159                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5160                sketch_block_ref
5161            )
5162            .into(),
5163        );
5164        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5165    };
5166    struct MarkSketchBlockBeingEdited<'a> {
5167        target_node_path: &'a ast::NodePath,
5168    }
5169
5170    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5171        type Break = ();
5172        type Continue = ();
5173
5174        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5175            if let NodeMut::SketchBlock(sketch_block) = node
5176                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5177            {
5178                sketch_block.is_being_edited = true;
5179                return TraversalReturn::new_break(());
5180            }
5181            TraversalReturn::new_continue(())
5182        }
5183
5184        fn finish(&mut self, _node: NodeMut<'_>) {}
5185    }
5186
5187    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5188    let found = dfs_mut(ast, &mut marker).is_break();
5189    if !found {
5190        return Err(KclError::refactor(format!(
5191            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5192        )));
5193    }
5194
5195    Ok(())
5196}
5197
5198fn sketch_on_ast_expr(
5199    ast: &mut ast::Node<ast::Program>,
5200    scene_graph: &SceneGraph,
5201    solid_references: &HashMap<Uuid, SolidAstReference>,
5202    on: &Plane,
5203) -> Result<ast::Expr, KclError> {
5204    match on {
5205        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5206        Plane::Object(object_id) => {
5207            let on_object = scene_graph
5208                .objects
5209                .get(object_id.0)
5210                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5211            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5212                return Ok(face_expr);
5213            }
5214            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5215        }
5216        Plane::PrimitiveFace(face) => {
5217            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5218                KclError::refactor(format!(
5219                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5220                    face.solid_id
5221                ))
5222            })?;
5223            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5224            Ok(create_face_of_ast(solid_expr, face_id_expr))
5225        }
5226    }
5227}
5228
5229fn solid_references_from_variables(
5230    ast: &ast::Node<ast::Program>,
5231    variables: &IndexMap<String, KclValueView>,
5232) -> HashMap<Uuid, SolidAstReference> {
5233    let mut references = HashMap::new();
5234
5235    // In-place operations such as shell reuse their input solid's engine ID.
5236    // Later variables overwrite earlier references so mutations target the result.
5237    for item in &ast.body {
5238        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5239            continue;
5240        };
5241        let name = &declaration.declaration.id.name;
5242        let Some(value) = variables.get(name) else {
5243            continue;
5244        };
5245
5246        match value {
5247            KclValueView::Solid { value } => {
5248                references.insert(
5249                    value.id,
5250                    SolidAstReference {
5251                        variable_name: name.clone(),
5252                        output_index: None,
5253                    },
5254                );
5255            }
5256            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5257                for (output_index, entry) in value.iter().enumerate() {
5258                    if let KclValueView::Solid { value } = entry {
5259                        references.insert(
5260                            value.id,
5261                            SolidAstReference {
5262                                variable_name: name.clone(),
5263                                output_index: Some(output_index),
5264                            },
5265                        );
5266                    }
5267                }
5268            }
5269            _ => {}
5270        }
5271    }
5272
5273    references
5274}
5275
5276fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5277    let reference = solid_references.get(&solid_id)?;
5278    let solid_expr = ast_name_expr(reference.variable_name.clone());
5279    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5280}
5281
5282fn sketch_face_of_scene_object_ast_expr(
5283    ast: &mut ast::Node<ast::Program>,
5284    on_object: &crate::front::Object,
5285) -> Result<Option<ast::Expr>, KclError> {
5286    match &on_object.kind {
5287        ObjectKind::Wall(wall) => {
5288            let solid_ref = get_or_insert_ast_reference(
5289                ast,
5290                &source_ref_from_source_ref_range(&wall.source.solid),
5291                "solid",
5292                None,
5293            )?;
5294            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5295                return Err(KclError::refactor(format!(
5296                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5297                    on_object.artifact_id
5298                )));
5299            };
5300            let solid_expr = indexed_solid_expr_for_sweep_output(
5301                ast_name_expr(solid_name_expr.name.name.clone()),
5302                wall.solid_output_index,
5303            );
5304            let sweep_ref = get_or_insert_ast_reference(
5305                ast,
5306                &source_ref_from_source_ref_range(&wall.source.sweep),
5307                "solid",
5308                None,
5309            )?;
5310            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5311                return Err(KclError::refactor(format!(
5312                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5313                    on_object.artifact_id
5314                )));
5315            };
5316            let sweep_name = sweep_name_expr.name.name.clone();
5317            let segment_ref = get_or_insert_ast_reference(
5318                ast,
5319                &source_ref_from_source_ref_range(&wall.source.segment),
5320                LINE_VARIABLE,
5321                None,
5322            )?;
5323
5324            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5325                wall.source
5326                    .path
5327                    .as_ref()
5328                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5329            }) {
5330                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5331                    return Err(KclError::refactor(format!(
5332                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5333                        on_object.artifact_id
5334                    )));
5335                };
5336                create_member_expression(
5337                    create_member_expression(ast_name_expr(region_name), "tags"),
5338                    &segment_name_expr.name.name,
5339                )
5340            } else {
5341                segment_ref
5342            };
5343
5344            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5345        }
5346        ObjectKind::Cap(cap) => {
5347            let solid_ref =
5348                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5349            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5350                return Err(KclError::refactor(format!(
5351                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5352                    on_object.artifact_id
5353                )));
5354            };
5355            let solid_expr = indexed_solid_expr_for_sweep_output(
5356                ast_name_expr(solid_name_expr.name.name.clone()),
5357                cap.solid_output_index,
5358            );
5359            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5360            let face_expr = match cap.kind {
5361                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5362                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5363            };
5364
5365            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5366        }
5367        _ => Ok(None),
5368    }
5369}
5370
5371fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5372    match solid_output_index {
5373        Some(output_index) => create_index_expression(solid_expr, output_index),
5374        None => solid_expr,
5375    }
5376}
5377
5378fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5379    SourceRef::Simple {
5380        range: source.range,
5381        node_path: source.node_path.clone(),
5382    }
5383}
5384
5385fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5386    let source_ref = source_ref_from_source_ref_range(path_source);
5387    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5388    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5389        return None;
5390    };
5391    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5392        return None;
5393    };
5394    if region_call.callee.name.name != "region" {
5395        return None;
5396    }
5397    Some(candidate)
5398}
5399
5400fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5401    let mut current_id = source_id;
5402    let mut current_composite = None;
5403    let mut visited = HashSet::new();
5404
5405    while visited.insert(current_id) {
5406        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5407
5408        let Some(composite_id) = next_composite_id else {
5409            break;
5410        };
5411        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5412            break;
5413        };
5414
5415        current_id = composite.id;
5416        current_composite = Some(composite);
5417
5418        if !composite.consumed {
5419            break;
5420        }
5421    }
5422
5423    current_composite.map(|composite| &composite.code_ref)
5424}
5425
5426fn downstream_composite_id_for_solid_source(
5427    artifact_graph: &ArtifactGraph,
5428    source_id: ArtifactId,
5429) -> Option<ArtifactId> {
5430    // Source is a path, find its solid.
5431    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5432        && let Some(composite_id) = path.composite_solid_id
5433        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5434        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5435    {
5436        return Some(composite_id);
5437    }
5438
5439    // Source is a sweep, find its path -> then find the solid
5440    for artifact in artifact_graph.values() {
5441        if let Artifact::Path(path) = artifact
5442            && path.sweep_id == Some(source_id)
5443            && let Some(composite_id) = path.composite_solid_id
5444            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5445            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5446        {
5447            return Some(composite_id);
5448        }
5449    }
5450
5451    // Source is a solid, find its downstream solid.
5452    artifact_graph.values().find_map(|artifact| {
5453        let Artifact::CompositeSolid(composite) = artifact else {
5454            return None;
5455        };
5456        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5457    })
5458}
5459
5460fn composite_contains_path_input(
5461    solid_ids: &[ArtifactId],
5462    tool_ids: &[ArtifactId],
5463    path_id: ArtifactId,
5464    solid2d_id: Option<ArtifactId>,
5465) -> bool {
5466    composite_contains_input(solid_ids, tool_ids, path_id)
5467        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5468}
5469
5470fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5471    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5472}
5473
5474fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5475    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5476    SourceRefRange {
5477        range: code_ref.range,
5478        node_path,
5479    }
5480}
5481
5482fn solid_output_index_for_sweep(
5483    artifact_graph: &ArtifactGraph,
5484    sweep_id: ArtifactId,
5485    sweep_code_ref: &CodeRef,
5486) -> Option<usize> {
5487    // Constituent sweeps are implementation details of one composite body,
5488    // even when cloning gives them the same CodeRef as the composite root.
5489    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5490        return None;
5491    }
5492
5493    let sibling_sweeps = artifact_graph
5494        .values()
5495        .filter_map(|artifact| match artifact {
5496            Artifact::Sweep(sweep)
5497                if sweep.code_ref.range == sweep_code_ref.range
5498                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5499            {
5500                Some(sweep)
5501            }
5502            _ => None,
5503        })
5504        .collect::<Vec<_>>();
5505
5506    if sibling_sweeps.len() <= 1 {
5507        return None;
5508    }
5509
5510    sibling_sweeps
5511        .iter()
5512        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5513}
5514
5515fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5516    let mut existing_artifact_ids = scene_objects
5517        .iter()
5518        .map(|object| object.artifact_id)
5519        .collect::<HashSet<_>>();
5520
5521    for artifact in artifact_graph.values() {
5522        match artifact {
5523            Artifact::Wall(wall) => {
5524                if existing_artifact_ids.contains(&wall.id) {
5525                    continue;
5526                }
5527
5528                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5529                    Artifact::Segment(segment) => Some(segment),
5530                    _ => None,
5531                }) else {
5532                    continue;
5533                };
5534                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5535                    Artifact::Sweep(sweep) => Some(sweep),
5536                    _ => None,
5537                }) else {
5538                    continue;
5539                };
5540                let source_segment = segment
5541                    .original_seg_id
5542                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5543                    .and_then(|artifact| match artifact {
5544                        Artifact::Segment(segment) => Some(segment),
5545                        _ => None,
5546                    })
5547                    .unwrap_or(segment);
5548                let solid_code_ref =
5549                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5550                let path_code_ref = artifact_graph
5551                    .get(&segment.path_id)
5552                    .or_else(|| artifact_graph.get(&sweep.path_id))
5553                    .and_then(|artifact| match artifact {
5554                        Artifact::Path(path) => Some(&path.code_ref),
5555                        _ => None,
5556                    });
5557                let source = WallSource {
5558                    solid: code_ref_source_ref_range(solid_code_ref),
5559                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5560                    path: path_code_ref.map(code_ref_source_ref_range),
5561                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5562                };
5563                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5564                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5565                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5566                    .flatten();
5567                let object_source = source_ref_from_source_ref_range(&source.solid);
5568                let id = ObjectId(scene_objects.len());
5569                scene_objects.push(crate::front::Object {
5570                    id,
5571                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5572                        id,
5573                        source,
5574                        solid_output_index,
5575                    }),
5576                    label: Default::default(),
5577                    comments: Default::default(),
5578                    artifact_id: wall.id,
5579                    source: object_source,
5580                });
5581                existing_artifact_ids.insert(wall.id);
5582            }
5583            Artifact::Cap(cap) => {
5584                if existing_artifact_ids.contains(&cap.id) {
5585                    continue;
5586                }
5587
5588                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5589                    Artifact::Sweep(sweep) => Some(sweep),
5590                    _ => None,
5591                }) else {
5592                    continue;
5593                };
5594                let id = ObjectId(scene_objects.len());
5595                let kind = match cap.sub_type {
5596                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5597                    CapSubType::End => crate::frontend::api::CapKind::End,
5598                };
5599                let solid_code_ref =
5600                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5601                let source = CapSource {
5602                    solid: code_ref_source_ref_range(solid_code_ref),
5603                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5604                };
5605                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5606                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5607                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5608                    .flatten();
5609                let object_source = source_ref_from_source_ref_range(&source.solid);
5610                scene_objects.push(crate::front::Object {
5611                    id,
5612                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5613                        id,
5614                        kind,
5615                        source,
5616                        solid_output_index,
5617                    }),
5618                    label: Default::default(),
5619                    comments: Default::default(),
5620                    artifact_id: cap.id,
5621                    source: object_source,
5622                });
5623                existing_artifact_ids.insert(cap.id);
5624            }
5625            _ => {}
5626        }
5627    }
5628}
5629
5630fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5631    use crate::engine::PlaneName;
5632
5633    match name {
5634        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5635        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5636        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5637        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5638        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5639        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5640    }
5641}
5642
5643fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5644    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5645        ast::UnaryOperator::Neg,
5646        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5647    )))
5648}
5649
5650fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5651    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5652        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5653        unlabeled: Some(solid_expr),
5654        arguments: vec![ast::LabeledArg {
5655            label: Some(ast::Identifier::new("face")),
5656            arg: face_expr,
5657        }],
5658        digest: None,
5659        non_code_meta: Default::default(),
5660    })))
5661}
5662
5663fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5664    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5665        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5666        unlabeled: Some(solid_expr),
5667        arguments: vec![ast::LabeledArg {
5668            label: Some(ast::Identifier::new("index")),
5669            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5670                ast::NumericLiteral {
5671                    value: index as f64,
5672                    suffix: NumericSuffix::None,
5673                    raw: index.to_string(),
5674                    digest: None,
5675                },
5676            )))),
5677        }],
5678        digest: None,
5679        non_code_meta: Default::default(),
5680    })))
5681}
5682
5683fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5684    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5685        return None;
5686    };
5687    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5688        return None;
5689    };
5690    if !matches!(
5691        sweep_call.callee.name.name.as_str(),
5692        "extrude" | "revolve" | "sweep" | "loft"
5693    ) {
5694        return None;
5695    }
5696    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5697        return None;
5698    };
5699    let candidate = region_name_expr.name.name.clone();
5700    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5701        return None;
5702    };
5703    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5704        return None;
5705    };
5706    if region_call.callee.name.name != "region" {
5707        return None;
5708    }
5709    Some(candidate)
5710}
5711
5712/// Return the AST expression referencing the variable at the given source ref.
5713/// If no such variable exists, insert a new variable declaration with the given
5714/// prefix.
5715///
5716/// This may return a complex expression referencing properties of the variable
5717/// (e.g., `line1.start`).
5718fn get_or_insert_ast_reference(
5719    ast: &mut ast::Node<ast::Program>,
5720    source_ref: &SourceRef,
5721    prefix: &str,
5722    property: Option<&str>,
5723) -> Result<ast::Expr, KclError> {
5724    let command = AstMutateCommand::AddVariableDeclaration {
5725        prefix: prefix.to_owned(),
5726    };
5727    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5728        Ok((_, ret)) => ret,
5729        Err(err) => {
5730            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5731                AstMutateCommandReturn::Name(var_name)
5732            } else {
5733                return Err(err);
5734            }
5735        }
5736    };
5737    let AstMutateCommandReturn::Name(var_name) = ret else {
5738        return Err(KclError::refactor(
5739            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5740        ));
5741    };
5742    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5743    let Some(property) = property else {
5744        // No property; just return the variable name.
5745        return Ok(var_expr);
5746    };
5747
5748    Ok(create_member_expression(var_expr, property))
5749}
5750
5751fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5752    let source_range = match source_ref {
5753        SourceRef::Simple { range, .. } => *range,
5754        SourceRef::BackTrace { ranges } => {
5755            let [range] = ranges.as_slice() else {
5756                return None;
5757            };
5758            range.0
5759        }
5760    };
5761    ast.body.iter().find_map(|item| {
5762        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5763            return None;
5764        };
5765        let init_range = SourceRange::from(&var_decl.declaration.init);
5766        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5767            && init_range.start() <= source_range.start()
5768            && source_range.end() <= init_range.end();
5769        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5770            && init_range != source_range
5771            && source_is_inside_init
5772        {
5773            return None;
5774        }
5775        source_is_inside_init.then(|| var_decl.name().to_owned())
5776    })
5777}
5778
5779fn mutate_ast_node_by_source_ref(
5780    ast: &mut ast::Node<ast::Program>,
5781    source_ref: &SourceRef,
5782    command: AstMutateCommand,
5783) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5784    let (source_range, node_path) = match source_ref {
5785        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5786        SourceRef::BackTrace { ranges } => {
5787            let [range] = ranges.as_slice() else {
5788                return Err(KclError::refactor(format!(
5789                    "Expected single source ref, got {}; ranges={ranges:#?}",
5790                    ranges.len(),
5791                )));
5792            };
5793            (range.0, range.1.clone())
5794        }
5795    };
5796    let mut context = AstMutateContext {
5797        source_range,
5798        node_path,
5799        command,
5800        defined_names_stack: Default::default(),
5801    };
5802    let control = dfs_mut(ast, &mut context);
5803    match control {
5804        ControlFlow::Continue(_) => Err(KclError::refactor(
5805            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5806        )),
5807        ControlFlow::Break(break_value) => break_value,
5808    }
5809}
5810
5811#[derive(Debug)]
5812struct AstMutateContext {
5813    source_range: SourceRange,
5814    node_path: Option<ast::NodePath>,
5815    command: AstMutateCommand,
5816    defined_names_stack: Vec<HashSet<String>>,
5817}
5818
5819#[derive(Debug)]
5820#[allow(clippy::large_enum_variant)]
5821enum AstMutateCommand {
5822    /// Add an expression statement to the sketch block.
5823    AddSketchBlockExprStmt {
5824        expr: ast::Expr,
5825    },
5826    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5827    AddSketchBlockVarDecl {
5828        prefix: String,
5829        expr: ast::Expr,
5830    },
5831    AddVariableDeclaration {
5832        prefix: String,
5833    },
5834    EditPoint {
5835        at: ast::Expr,
5836    },
5837    EditLine {
5838        start: ast::Expr,
5839        end: ast::Expr,
5840        construction: Option<bool>,
5841    },
5842    EditArc {
5843        start: ast::Expr,
5844        end: ast::Expr,
5845        center: ast::Expr,
5846        direction: Option<ArcDirection>,
5847        construction: Option<bool>,
5848    },
5849    EditCircle {
5850        start: ast::Expr,
5851        center: ast::Expr,
5852        construction: Option<bool>,
5853    },
5854    EditControlPointSpline {
5855        points: ast::Expr,
5856        construction: Option<bool>,
5857    },
5858    EditConstraintValue {
5859        value: ast::BinaryPart,
5860    },
5861    EditAngleConstraint {
5862        call: ast::BinaryPart,
5863        value: ast::BinaryPart,
5864    },
5865    EditDistanceConstraint {
5866        call: ast::BinaryPart,
5867        value: ast::BinaryPart,
5868    },
5869    EditDistanceConstraintLabelPosition {
5870        label_position: ast::Expr,
5871    },
5872    EditCallUnlabeled {
5873        arg: ast::Expr,
5874    },
5875    EditVarInitialValue {
5876        value: Number,
5877    },
5878    DeleteNode,
5879}
5880
5881impl AstMutateCommand {
5882    fn needs_defined_names_stack(&self) -> bool {
5883        matches!(
5884            self,
5885            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5886        )
5887    }
5888}
5889
5890#[derive(Debug)]
5891enum AstMutateCommandReturn {
5892    None,
5893    Name(String),
5894}
5895
5896#[derive(Debug, Clone)]
5897struct AstNodeRef {
5898    range: SourceRange,
5899    node_path: Option<ast::NodePath>,
5900}
5901
5902impl<T> From<&ast::Node<T>> for AstNodeRef {
5903    fn from(value: &ast::Node<T>) -> Self {
5904        AstNodeRef {
5905            range: value.into(),
5906            node_path: value.node_path.clone(),
5907        }
5908    }
5909}
5910
5911impl From<&ast::BodyItem> for AstNodeRef {
5912    fn from(value: &ast::BodyItem) -> Self {
5913        match value {
5914            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5915                range: node.into(),
5916                node_path: node.node_path.clone(),
5917            },
5918            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5919                range: node.into(),
5920                node_path: node.node_path.clone(),
5921            },
5922            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5923                range: node.into(),
5924                node_path: node.node_path.clone(),
5925            },
5926            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5927                range: node.into(),
5928                node_path: node.node_path.clone(),
5929            },
5930            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5931                range: node.into(),
5932                node_path: node.node_path.clone(),
5933            },
5934        }
5935    }
5936}
5937
5938impl From<&ast::Expr> for AstNodeRef {
5939    fn from(value: &ast::Expr) -> Self {
5940        AstNodeRef {
5941            range: SourceRange::from(value),
5942            node_path: value.node_path().cloned(),
5943        }
5944    }
5945}
5946
5947impl From<&AstMutateContext> for AstNodeRef {
5948    fn from(value: &AstMutateContext) -> Self {
5949        AstNodeRef {
5950            range: value.source_range,
5951            node_path: value.node_path.clone(),
5952        }
5953    }
5954}
5955
5956impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5957    type Error = crate::walk::AstNodeError;
5958
5959    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5960        Ok(AstNodeRef {
5961            range: SourceRange::try_from(value)?,
5962            node_path: value.try_into()?,
5963        })
5964    }
5965}
5966
5967impl From<AstNodeRef> for SourceRange {
5968    fn from(value: AstNodeRef) -> Self {
5969        value.range
5970    }
5971}
5972
5973impl Visitor for AstMutateContext {
5974    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5975    type Continue = ();
5976
5977    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5978        filter_and_process(self, node)
5979    }
5980
5981    fn finish(&mut self, node: NodeMut<'_>) {
5982        match &node {
5983            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5984                self.defined_names_stack.pop();
5985            }
5986            _ => {}
5987        }
5988    }
5989}
5990
5991fn filter_and_process(
5992    ctx: &mut AstMutateContext,
5993    node: NodeMut,
5994) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5995    let Ok(node_range) = SourceRange::try_from(&node) else {
5996        // Nodes that can't be converted to a range aren't interesting.
5997        return TraversalReturn::new_continue(());
5998    };
5999    // If we're adding a variable declaration, we need to look at variable
6000    // declaration expressions to see if it already has a variable, before
6001    // continuing. The variable declaration's source range won't match the
6002    // target; its init expression will.
6003    if let NodeMut::VariableDeclaration(var_decl) = &node {
6004        let expr_range = SourceRange::from(&var_decl.declaration.init);
6005        let expr_node_path = var_decl.declaration.init.node_path();
6006        if source_ref_matches(ctx, expr_range, expr_node_path) {
6007            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6008                // We found the variable declaration expression. It doesn't need
6009                // to be added.
6010                return TraversalReturn::new_break(Ok((
6011                    AstNodeRef::from(&**var_decl),
6012                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6013                )));
6014            }
6015            if let AstMutateCommand::DeleteNode = &ctx.command {
6016                // We found the variable declaration. Delete the variable along
6017                // with the segment.
6018                return TraversalReturn {
6019                    mutate_body_item: MutateBodyItem::Delete,
6020                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6021                };
6022            }
6023        }
6024    }
6025    // Similar thing with expression statement. We need to look at the
6026    // expression inside it.
6027    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6028        let expr_range = SourceRange::from(&expr_stmt.expression);
6029        let expr_node_path = expr_stmt.expression.node_path();
6030        if source_ref_matches(ctx, expr_range, expr_node_path) {
6031            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6032                // We found the node wrapped in an expression statement. Process
6033                // the statement.
6034                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6035                    return TraversalReturn::new_continue(());
6036                };
6037                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6038            }
6039            if let AstMutateCommand::DeleteNode = &ctx.command {
6040                // We found the node wrapped in an expression statement. Delete
6041                // the whole statement.
6042                return TraversalReturn {
6043                    mutate_body_item: MutateBodyItem::Delete,
6044                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6045                };
6046            }
6047        }
6048    }
6049
6050    if ctx.command.needs_defined_names_stack() {
6051        if let NodeMut::Program(program) = &node {
6052            ctx.defined_names_stack.push(find_defined_names(*program));
6053        } else if let NodeMut::SketchBlock(block) = &node {
6054            ctx.defined_names_stack.push(find_defined_names(&block.body));
6055        }
6056    }
6057
6058    // Make sure the node matches the source ref.
6059    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6060    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6061        return TraversalReturn::new_continue(());
6062    }
6063    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6064        return TraversalReturn::new_continue(());
6065    };
6066    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6067}
6068
6069fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6070    match &ctx.node_path {
6071        Some(target) => Some(target) == node_path,
6072        None => node_range == ctx.source_range,
6073    }
6074}
6075
6076fn is_angle_constraint_call_name(name: &str) -> bool {
6077    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6078}
6079
6080fn is_distance_constraint_call_name(name: &str) -> bool {
6081    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6082}
6083
6084fn is_constraint_call_name(name: &str) -> bool {
6085    matches!(
6086        name,
6087        DISTANCE_FN
6088            | HORIZONTAL_DISTANCE_FN
6089            | VERTICAL_DISTANCE_FN
6090            | RADIUS_FN
6091            | DIAMETER_FN
6092            | ANGLE_FN
6093            | ANGLE_DIMENSION_FN
6094    )
6095}
6096
6097fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6098    if let ast::BinaryPart::CallExpressionKw(call) = part
6099        && is_constraint_call_name(call.callee.name.name.as_str())
6100    {
6101        Some(call)
6102    } else {
6103        None
6104    }
6105}
6106
6107fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6108    match &ctx.command {
6109        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6110            if let NodeMut::SketchBlock(sketch_block) = node {
6111                sketch_block
6112                    .body
6113                    .items
6114                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6115                        inner: ast::ExpressionStatement {
6116                            expression: expr.clone(),
6117                            digest: None,
6118                        },
6119                        start: Default::default(),
6120                        end: Default::default(),
6121                        module_id: Default::default(),
6122                        node_path: None,
6123                        outer_attrs: Default::default(),
6124                        pre_comments: Default::default(),
6125                        comment_start: Default::default(),
6126                    }));
6127                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128            }
6129        }
6130        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6131            if let NodeMut::SketchBlock(sketch_block) = node {
6132                let empty_defined_names = HashSet::new();
6133                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6134                let Ok(name) = next_free_name(prefix, defined_names) else {
6135                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6136                };
6137                sketch_block
6138                    .body
6139                    .items
6140                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6141                        ast::VariableDeclaration::new(
6142                            ast::VariableDeclarator::new(&name, expr.clone()),
6143                            ast::ItemVisibility::Default,
6144                            ast::VariableKind::Const,
6145                        ),
6146                    ))));
6147                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6148            }
6149        }
6150        AstMutateCommand::AddVariableDeclaration { prefix } => {
6151            if let NodeMut::VariableDeclaration(inner) = node {
6152                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6153            }
6154            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6155                let empty_defined_names = HashSet::new();
6156                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6157                let Ok(name) = next_free_name(prefix, defined_names) else {
6158                    // TODO: Return an error instead?
6159                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6160                };
6161                let mutate_node =
6162                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6163                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6164                        ast::ItemVisibility::Default,
6165                        ast::VariableKind::Const,
6166                    ))));
6167                return TraversalReturn {
6168                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6169                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6170                };
6171            }
6172        }
6173        AstMutateCommand::EditPoint { at } => {
6174            if let NodeMut::CallExpressionKw(call) = node {
6175                if call.callee.name.name != POINT_FN {
6176                    return TraversalReturn::new_continue(());
6177                }
6178                // Update the arguments.
6179                for labeled_arg in &mut call.arguments {
6180                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6181                        labeled_arg.arg = at.clone();
6182                    }
6183                }
6184                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6185            }
6186        }
6187        AstMutateCommand::EditLine {
6188            start,
6189            end,
6190            construction,
6191        } => {
6192            if let NodeMut::CallExpressionKw(call) = node {
6193                if call.callee.name.name != LINE_FN {
6194                    return TraversalReturn::new_continue(());
6195                }
6196                // Update the arguments.
6197                for labeled_arg in &mut call.arguments {
6198                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6199                        labeled_arg.arg = start.clone();
6200                    }
6201                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6202                        labeled_arg.arg = end.clone();
6203                    }
6204                }
6205                // Handle construction kwarg
6206                if let Some(construction_value) = construction {
6207                    let construction_exists = call
6208                        .arguments
6209                        .iter()
6210                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6211                    if *construction_value {
6212                        // Add or update construction=true
6213                        if construction_exists {
6214                            // Update existing construction kwarg
6215                            for labeled_arg in &mut call.arguments {
6216                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6217                                    labeled_arg.arg =
6218                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6219                                            value: ast::LiteralValue::Bool(true),
6220                                            raw: "true".to_string(),
6221                                            digest: None,
6222                                        })));
6223                                }
6224                            }
6225                        } else {
6226                            // Add new construction kwarg
6227                            call.arguments.push(ast::LabeledArg {
6228                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6229                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6230                                    value: ast::LiteralValue::Bool(true),
6231                                    raw: "true".to_string(),
6232                                    digest: None,
6233                                }))),
6234                            });
6235                        }
6236                    } else {
6237                        // Remove construction kwarg if it exists
6238                        call.arguments
6239                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6240                    }
6241                }
6242                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6243            }
6244        }
6245        AstMutateCommand::EditArc {
6246            start,
6247            end,
6248            center,
6249            direction,
6250            construction,
6251        } => {
6252            if let NodeMut::CallExpressionKw(call) = node {
6253                if call.callee.name.name != ARC_FN {
6254                    return TraversalReturn::new_continue(());
6255                }
6256                // Update the arguments.
6257                for labeled_arg in &mut call.arguments {
6258                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6259                        labeled_arg.arg = start.clone();
6260                    }
6261                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6262                        labeled_arg.arg = end.clone();
6263                    }
6264                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6265                        labeled_arg.arg = center.clone();
6266                    }
6267                }
6268                // Handle direction kwarg
6269                if let Some(direction_value) = direction {
6270                    let direction_exists = call
6271                        .arguments
6272                        .iter()
6273                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6274                    if direction_value.is_clockwise() {
6275                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6276                        if direction_exists {
6277                            // Update existing direction kwarg
6278                            for labeled_arg in &mut call.arguments {
6279                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6280                                    labeled_arg.arg = direction_ast.clone();
6281                                }
6282                            }
6283                        } else {
6284                            // Add new direction kwarg
6285                            call.arguments.push(ast::LabeledArg {
6286                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6287                                arg: direction_ast,
6288                            });
6289                        }
6290                    } else {
6291                        // Remove direction kwarg if it exists since
6292                        // counterclockwise is the default
6293                        call.arguments
6294                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6295                    }
6296                }
6297                // Handle construction kwarg
6298                if let Some(construction_value) = construction {
6299                    let construction_exists = call
6300                        .arguments
6301                        .iter()
6302                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6303                    if *construction_value {
6304                        // Add or update construction=true
6305                        if construction_exists {
6306                            // Update existing construction kwarg
6307                            for labeled_arg in &mut call.arguments {
6308                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6309                                    labeled_arg.arg =
6310                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6311                                            value: ast::LiteralValue::Bool(true),
6312                                            raw: "true".to_string(),
6313                                            digest: None,
6314                                        })));
6315                                }
6316                            }
6317                        } else {
6318                            // Add new construction kwarg
6319                            call.arguments.push(ast::LabeledArg {
6320                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6321                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6322                                    value: ast::LiteralValue::Bool(true),
6323                                    raw: "true".to_string(),
6324                                    digest: None,
6325                                }))),
6326                            });
6327                        }
6328                    } else {
6329                        // Remove construction kwarg if it exists
6330                        call.arguments
6331                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6332                    }
6333                }
6334                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6335            }
6336        }
6337        AstMutateCommand::EditCircle {
6338            start,
6339            center,
6340            construction,
6341        } => {
6342            if let NodeMut::CallExpressionKw(call) = node {
6343                if call.callee.name.name != CIRCLE_FN {
6344                    return TraversalReturn::new_continue(());
6345                }
6346                // Update the arguments.
6347                for labeled_arg in &mut call.arguments {
6348                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6349                        labeled_arg.arg = start.clone();
6350                    }
6351                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6352                        labeled_arg.arg = center.clone();
6353                    }
6354                }
6355                // Handle construction kwarg
6356                if let Some(construction_value) = construction {
6357                    let construction_exists = call
6358                        .arguments
6359                        .iter()
6360                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6361                    if *construction_value {
6362                        if construction_exists {
6363                            // Update existing construction kwarg
6364                            for labeled_arg in &mut call.arguments {
6365                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6366                                    labeled_arg.arg =
6367                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6368                                            value: ast::LiteralValue::Bool(true),
6369                                            raw: "true".to_string(),
6370                                            digest: None,
6371                                        })));
6372                                }
6373                            }
6374                        } else {
6375                            // Add new construction kwarg
6376                            call.arguments.push(ast::LabeledArg {
6377                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6378                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6379                                    value: ast::LiteralValue::Bool(true),
6380                                    raw: "true".to_string(),
6381                                    digest: None,
6382                                }))),
6383                            });
6384                        }
6385                    } else {
6386                        // Remove construction kwarg if it exists
6387                        call.arguments
6388                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6389                    }
6390                }
6391                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6392            }
6393        }
6394        AstMutateCommand::EditControlPointSpline { points, construction } => {
6395            if let NodeMut::CallExpressionKw(call) = node {
6396                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6397                    return TraversalReturn::new_continue(());
6398                }
6399                for labeled_arg in &mut call.arguments {
6400                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6401                    {
6402                        labeled_arg.arg = points.clone();
6403                    }
6404                }
6405                // Handle construction kwarg
6406                if let Some(construction_value) = construction {
6407                    let construction_exists = call
6408                        .arguments
6409                        .iter()
6410                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6411                    if *construction_value {
6412                        if construction_exists {
6413                            for labeled_arg in &mut call.arguments {
6414                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6415                                    labeled_arg.arg =
6416                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6417                                            value: ast::LiteralValue::Bool(true),
6418                                            raw: "true".to_string(),
6419                                            digest: None,
6420                                        })));
6421                                }
6422                            }
6423                        } else {
6424                            call.arguments.push(ast::LabeledArg {
6425                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6426                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6427                                    value: ast::LiteralValue::Bool(true),
6428                                    raw: "true".to_string(),
6429                                    digest: None,
6430                                }))),
6431                            });
6432                        }
6433                    } else {
6434                        call.arguments
6435                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6436                    }
6437                }
6438                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6439            }
6440        }
6441        AstMutateCommand::EditConstraintValue { value } => {
6442            if let NodeMut::BinaryExpression(binary_expr) = node {
6443                let left_is_constraint = matches!(
6444                    &binary_expr.left,
6445                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6446                );
6447                if left_is_constraint {
6448                    binary_expr.right = value.clone();
6449                } else {
6450                    binary_expr.left = value.clone();
6451                }
6452
6453                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6454            }
6455        }
6456        AstMutateCommand::EditAngleConstraint { call, value } => {
6457            if let NodeMut::BinaryExpression(binary_expr) = node {
6458                let left_is_angle = matches!(
6459                    &binary_expr.left,
6460                    ast::BinaryPart::CallExpressionKw(existing_call)
6461                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6462                );
6463                let right_is_angle = matches!(
6464                    &binary_expr.right,
6465                    ast::BinaryPart::CallExpressionKw(existing_call)
6466                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6467                );
6468
6469                match (left_is_angle, right_is_angle) {
6470                    (true, _) => {
6471                        binary_expr.left = call.clone();
6472                        binary_expr.right = value.clone();
6473                    }
6474                    (false, true) => {
6475                        binary_expr.left = value.clone();
6476                        binary_expr.right = call.clone();
6477                    }
6478                    (false, false) => return TraversalReturn::new_continue(()),
6479                }
6480
6481                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6482            }
6483        }
6484        AstMutateCommand::EditDistanceConstraint { call, value } => {
6485            if let NodeMut::BinaryExpression(binary_expr) = node {
6486                let left_is_distance = matches!(
6487                    &binary_expr.left,
6488                    ast::BinaryPart::CallExpressionKw(existing_call)
6489                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6490                );
6491                let right_is_distance = matches!(
6492                    &binary_expr.right,
6493                    ast::BinaryPart::CallExpressionKw(existing_call)
6494                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6495                );
6496
6497                match (left_is_distance, right_is_distance) {
6498                    (true, _) => {
6499                        binary_expr.left = call.clone();
6500                        binary_expr.right = value.clone();
6501                    }
6502                    (false, true) => {
6503                        binary_expr.left = value.clone();
6504                        binary_expr.right = call.clone();
6505                    }
6506                    (false, false) => return TraversalReturn::new_continue(()),
6507                }
6508
6509                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6510            }
6511        }
6512        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6513            if let NodeMut::BinaryExpression(binary_expr) = node {
6514                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6515                    call
6516                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6517                    call
6518                } else {
6519                    return TraversalReturn::new_continue(());
6520                };
6521
6522                if let Some(label_arg) = call
6523                    .arguments
6524                    .iter_mut()
6525                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6526                {
6527                    label_arg.arg = label_position.clone();
6528                } else {
6529                    call.arguments.push(ast::LabeledArg {
6530                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6531                        arg: label_position.clone(),
6532                    });
6533                }
6534
6535                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536            }
6537        }
6538        AstMutateCommand::EditCallUnlabeled { arg } => {
6539            if let NodeMut::CallExpressionKw(call) = node {
6540                call.unlabeled = Some(arg.clone());
6541                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6542            }
6543        }
6544        AstMutateCommand::EditVarInitialValue { value } => {
6545            // We target the SketchVar itself (matched by NodePath) rather than
6546            // the inner NumericLiteral so we can also write back into vars that
6547            // were declared without an initial value (e.g. bare `var`).
6548            if let NodeMut::SketchVar(sketch_var) = node {
6549                let Ok(literal) = to_source_number(*value) else {
6550                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6551                        "Could not convert number to AST literal: {:?}",
6552                        *value
6553                    ))));
6554                };
6555                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6556                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6557            }
6558        }
6559        AstMutateCommand::DeleteNode => {
6560            return TraversalReturn {
6561                mutate_body_item: MutateBodyItem::Delete,
6562                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6563            };
6564        }
6565    }
6566    TraversalReturn::new_continue(())
6567}
6568
6569struct FindSketchBlockSourceRange {
6570    /// The source range of the sketch block before mutation.
6571    target_before_mutation: SourceRange,
6572    /// The source range of the sketch block's last body item after mutation. We
6573    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6574    /// shared reference.
6575    found: Cell<Option<AstNodeRef>>,
6576}
6577
6578impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6579    type Error = crate::front::Error;
6580
6581    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6582        let Ok(node_range) = SourceRange::try_from(&node) else {
6583            return Ok(true);
6584        };
6585
6586        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6587            if node_range.module_id() == self.target_before_mutation.module_id()
6588                && node_range.start() == self.target_before_mutation.start()
6589                // End shouldn't match since we added something.
6590                && node_range.end() >= self.target_before_mutation.end()
6591            {
6592                self.found.set(sketch_block.body.items.last().map(|item| match item {
6593                    // For declarations like `circle1 = circle(...)`, use
6594                    // the init expression range so lookup in source_range_to_object
6595                    // matches the segment source range.
6596                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6597                    _ => AstNodeRef::from(item),
6598                }));
6599                return Ok(false);
6600            } else {
6601                // We found a different sketch block. No need to descend into
6602                // its children since sketch blocks cannot be nested.
6603                return Ok(true);
6604            }
6605        }
6606
6607        for child in node.children().iter() {
6608            if !child.visit(*self)? {
6609                return Ok(false);
6610            }
6611        }
6612
6613        Ok(true)
6614    }
6615}
6616
6617struct FindSketchBlockByNodePath {
6618    /// The Node Path of the sketch block before mutation.
6619    target_node_path: ast::NodePath,
6620    /// The ref of the sketch block's last body item after mutation. We need to
6621    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6622    /// reference.
6623    found: Cell<Option<AstNodeRef>>,
6624}
6625
6626impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6627    type Error = crate::front::Error;
6628
6629    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6630        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6631            return Ok(true);
6632        };
6633
6634        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6635            if let Some(node_path) = node_path
6636                && node_path == self.target_node_path
6637            {
6638                self.found.set(sketch_block.body.items.last().map(|item| match item {
6639                    // For declarations like `circle1 = circle(...)`, use
6640                    // the init expression range so lookup in source_range_to_object
6641                    // matches the segment source range.
6642                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6643                    _ => AstNodeRef::from(item),
6644                }));
6645
6646                return Ok(false);
6647            } else {
6648                // We found a different sketch block. No need to descend into
6649                // its children since sketch blocks cannot be nested.
6650                return Ok(true);
6651            }
6652        }
6653
6654        for child in node.children().iter() {
6655            if !child.visit(*self)? {
6656                return Ok(false);
6657            }
6658        }
6659
6660        Ok(true)
6661    }
6662}
6663
6664/// After adding an item to a sketch block, find the sketch block, and get the
6665/// source range of the added item. We assume that the added item is the last
6666/// item in the sketch block and that the sketch block's source range has grown,
6667/// but not moved from its starting offset.
6668///
6669/// TODO: Do we need to format *before* mutation in case formatting moves the
6670/// sketch block forward?
6671fn find_sketch_block_added_item(
6672    ast: &ast::Node<ast::Program>,
6673    sketch_block_before_mutation: &AstNodeRef,
6674) -> Result<AstNodeRef, KclError> {
6675    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6676        let find = FindSketchBlockByNodePath {
6677            target_node_path: node_path.clone(),
6678            found: Cell::new(None),
6679        };
6680        let node = crate::walk::Node::from(ast);
6681        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6682        find.found.into_inner().ok_or_else(|| {
6683            KclError::refactor(format!(
6684                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6685            ))
6686        })
6687    } else {
6688        // No NodePath. Fall back to legacy source range.
6689        let find = FindSketchBlockSourceRange {
6690            target_before_mutation: sketch_block_before_mutation.range,
6691            found: Cell::new(None),
6692        };
6693        let node = crate::walk::Node::from(ast);
6694        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6695        find.found.into_inner().ok_or_else(|| KclError::refactor(
6696            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?"),
6697        ))
6698    }
6699}
6700
6701fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6702    let message = error.message().trim();
6703    let message = if message.is_empty() {
6704        "unknown parse error"
6705    } else {
6706        message
6707    };
6708
6709    format!("{prefix}: {message}")
6710}
6711
6712fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6713    let (program, errors) = Program::parse(source).map_err(|err| {
6714        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6715    })?;
6716    if !errors.is_empty() {
6717        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6718            format_compilation_issues(parse_error_prefix, &errors),
6719        )));
6720    }
6721
6722    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6723}
6724
6725fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6726    let Some(first_issue) = issues
6727        .iter()
6728        .find(|issue| issue.severity.is_err())
6729        .or_else(|| issues.first())
6730    else {
6731        return prefix.to_owned();
6732    };
6733
6734    let message = first_issue.message.trim();
6735    let message = if message.is_empty() {
6736        "unknown parse error"
6737    } else {
6738        message
6739    };
6740
6741    if issues.len() > 1 {
6742        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6743    } else {
6744        format!("{prefix}: {message}")
6745    }
6746}
6747
6748fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6749    // TODO: Don't duplicate this from lib.rs Program.
6750    ast.recast_top(&Default::default(), 0)
6751}
6752
6753struct FindNumericLiteral {
6754    target: SourceRange,
6755    found: Cell<Option<ast::NumericLiteral>>,
6756}
6757
6758impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6759    type Error = crate::front::Error;
6760
6761    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6762        let Ok(node_range) = SourceRange::try_from(&node) else {
6763            return Ok(true);
6764        };
6765
6766        if node_range == self.target
6767            && let crate::walk::Node::NumericLiteral(literal) = node
6768        {
6769            self.found.set(Some(literal.inner.clone()));
6770            return Ok(false);
6771        }
6772
6773        for child in node.children().iter() {
6774            if !child.visit(*self)? {
6775                return Ok(false);
6776            }
6777        }
6778
6779        Ok(true)
6780    }
6781}
6782
6783fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6784    let find = FindNumericLiteral {
6785        target,
6786        found: Cell::new(None),
6787    };
6788    let node = crate::walk::Node::from(ast);
6789    node.visit(&find).ok()?;
6790    find.found.into_inner()
6791}
6792
6793struct FindSketchVarInitialByNodePath<'a> {
6794    target: &'a ast::NodePath,
6795    sketch_var_found: Cell<bool>,
6796    initial_literal: Cell<Option<ast::NumericLiteral>>,
6797}
6798
6799impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6800    type Error = crate::front::Error;
6801
6802    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6803        if let crate::walk::Node::SketchVar(sketch_var) = node
6804            && sketch_var.node_path.as_ref() == Some(self.target)
6805        {
6806            self.sketch_var_found.set(true);
6807            if let Some(initial) = &sketch_var.initial {
6808                self.initial_literal.set(Some(initial.inner.clone()));
6809            }
6810            return Ok(false);
6811        }
6812
6813        for child in node.children().iter() {
6814            if !child.visit(*self)? {
6815                return Ok(false);
6816            }
6817        }
6818
6819        Ok(true)
6820    }
6821}
6822
6823/// Locate the source `var` declaration corresponding to a sketch-var solution.
6824///
6825/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6826/// from "target found." The inner [`Option`] is the initial numeric literal of
6827/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6828///
6829/// When `node_path` is `None` (e.g. for older outcomes that predate the
6830/// node-path propagation), this falls back to source-range matching, which
6831/// can break under whitespace shifts elsewhere in the file.
6832fn numeric_literal_at_node_path(
6833    ast: &ast::Node<ast::Program>,
6834    node_path: Option<&ast::NodePath>,
6835    source_range: SourceRange,
6836) -> Option<Option<ast::NumericLiteral>> {
6837    let Some(node_path) = node_path else {
6838        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";
6839        #[cfg(target_arch = "wasm32")]
6840        web_sys::console::warn_1(&message.into());
6841        #[cfg(not(target_arch = "wasm32"))]
6842        eprintln!("WARNING: {message}");
6843        return numeric_literal_at_source_range(ast, source_range).map(Some);
6844    };
6845    let find = FindSketchVarInitialByNodePath {
6846        target: node_path,
6847        sketch_var_found: Cell::new(false),
6848        initial_literal: Cell::new(None),
6849    };
6850    let node = crate::walk::Node::from(ast);
6851    node.visit(&find).ok()?;
6852    if !find.sketch_var_found.get() {
6853        return None;
6854    }
6855    Some(find.initial_literal.into_inner())
6856}
6857
6858fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6859    match suffix {
6860        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6861        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6862        NumericSuffix::M => Some(UnitLength::Meters),
6863        NumericSuffix::Inch => Some(UnitLength::Inches),
6864        NumericSuffix::Ft => Some(UnitLength::Feet),
6865        NumericSuffix::Yd => Some(UnitLength::Yards),
6866        _ => None,
6867    }
6868}
6869
6870fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6871    match suffix_length_unit(number.units) {
6872        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6873        None => number.value,
6874    }
6875}
6876
6877fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6878    match suffix_length_unit(literal.suffix) {
6879        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6880        None => literal.value,
6881    }
6882}
6883
6884fn var_solution_needs_commit(
6885    current_literal: &ast::NumericLiteral,
6886    solved_value: Number,
6887    default_length_unit: UnitLength,
6888) -> bool {
6889    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6890    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6891
6892    (current - solved).abs() > 1e-9
6893}
6894
6895fn preserve_var_solution_literal_style(
6896    current_literal: &ast::NumericLiteral,
6897    solved_value: Number,
6898    default_length_unit: UnitLength,
6899) -> Number {
6900    if current_literal.suffix == NumericSuffix::None {
6901        return Number {
6902            value: number_value_in_default_length_units(solved_value, default_length_unit),
6903            units: NumericSuffix::None,
6904        };
6905    }
6906
6907    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6908        return solved_value;
6909    };
6910
6911    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6912    Number {
6913        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6914        units: current_literal.suffix,
6915    }
6916}
6917
6918pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6919    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6920        inner: ast::ArrayExpression {
6921            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6922            non_code_meta: Default::default(),
6923            digest: None,
6924        },
6925        start: Default::default(),
6926        end: Default::default(),
6927        module_id: Default::default(),
6928        node_path: None,
6929        outer_attrs: Default::default(),
6930        pre_comments: Default::default(),
6931        comment_start: Default::default(),
6932    })))
6933}
6934
6935pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6936    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6937        ast::ArrayExpression {
6938            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6939            digest: None,
6940            non_code_meta: Default::default(),
6941        },
6942    ))))
6943}
6944
6945fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6946    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6947        ast::ArrayExpression {
6948            elements: vec![
6949                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6950                    point.x,
6951                )?)))),
6952                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6953                    point.y,
6954                )?)))),
6955            ],
6956            non_code_meta: Default::default(),
6957            digest: None,
6958        },
6959    ))))
6960}
6961
6962fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6963    match expr {
6964        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6965            inner: ast::Literal::from(to_source_number(*number)?),
6966            start: Default::default(),
6967            end: Default::default(),
6968            module_id: Default::default(),
6969            node_path: None,
6970            outer_attrs: Default::default(),
6971            pre_comments: Default::default(),
6972            comment_start: Default::default(),
6973        }))),
6974        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6975            inner: ast::SketchVar {
6976                initial: Some(BoxNode::new(ast::Node {
6977                    inner: to_source_number(*number)?,
6978                    start: Default::default(),
6979                    end: Default::default(),
6980                    module_id: Default::default(),
6981                    node_path: None,
6982                    outer_attrs: Default::default(),
6983                    pre_comments: Default::default(),
6984                    comment_start: Default::default(),
6985                })),
6986                digest: None,
6987            },
6988            start: Default::default(),
6989            end: Default::default(),
6990            module_id: Default::default(),
6991            node_path: None,
6992            outer_attrs: Default::default(),
6993            pre_comments: Default::default(),
6994            comment_start: Default::default(),
6995        }))),
6996        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6997    }
6998}
6999
7000fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7001    Ok(ast::NumericLiteral {
7002        value: number.value,
7003        suffix: number.units,
7004        raw: format_number_literal(number.value, number.units, None)?,
7005        digest: None,
7006    })
7007}
7008
7009pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7010    ast::Expr::Name(BoxNode::new(ast_name(name)))
7011}
7012
7013fn ast_name(name: String) -> ast::Node<ast::Name> {
7014    ast::Node {
7015        inner: ast::Name {
7016            name: ast::Node {
7017                inner: ast::Identifier { name, digest: None },
7018                start: Default::default(),
7019                end: Default::default(),
7020                module_id: Default::default(),
7021                node_path: None,
7022                outer_attrs: Default::default(),
7023                pre_comments: Default::default(),
7024                comment_start: Default::default(),
7025            },
7026            path: Vec::new(),
7027            abs_path: false,
7028            digest: None,
7029        },
7030        start: Default::default(),
7031        end: Default::default(),
7032        module_id: Default::default(),
7033        node_path: None,
7034        outer_attrs: Default::default(),
7035        pre_comments: Default::default(),
7036        comment_start: Default::default(),
7037    }
7038}
7039
7040pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7041    ast::Name {
7042        name: ast::Node {
7043            inner: ast::Identifier {
7044                name: name.to_owned(),
7045                digest: None,
7046            },
7047            start: Default::default(),
7048            end: Default::default(),
7049            module_id: Default::default(),
7050            node_path: None,
7051            outer_attrs: Default::default(),
7052            pre_comments: Default::default(),
7053            comment_start: Default::default(),
7054        },
7055        path: Default::default(),
7056        abs_path: false,
7057        digest: None,
7058    }
7059}
7060
7061/// Create an AST node for coincident([expr1, expr2, ...])
7062pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7063    let elements = exprs.into_iter().collect::<Vec<_>>();
7064    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7065
7066    // Create array [expr1, expr2, ...]
7067    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7068        elements,
7069        digest: None,
7070        non_code_meta: Default::default(),
7071    })));
7072
7073    // Create coincident([...])
7074    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7075        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7076        unlabeled: Some(array_expr),
7077        arguments: Default::default(),
7078        digest: None,
7079        non_code_meta: Default::default(),
7080    })))
7081}
7082
7083/// Create an AST node for horizontal(line)
7084pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7085    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7086        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7087        unlabeled: Some(line_expr),
7088        arguments: Default::default(),
7089        digest: None,
7090        non_code_meta: Default::default(),
7091    })))
7092}
7093
7094/// Create an AST node for vertical(line)
7095pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7096    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7097        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7098        unlabeled: Some(line_expr),
7099        arguments: Default::default(),
7100        digest: None,
7101        non_code_meta: Default::default(),
7102    })))
7103}
7104
7105/// Create a member expression like object.property (e.g., line1.end)
7106pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7107    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7108        object: object_expr,
7109        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7110            name: ast::Node::no_src(ast::Identifier {
7111                name: property.to_string(),
7112                digest: None,
7113            }),
7114            path: Vec::new(),
7115            abs_path: false,
7116            digest: None,
7117        }))),
7118        computed: false,
7119        digest: None,
7120    })))
7121}
7122
7123pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7124    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7125        object: object_expr,
7126        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7127            ast::NumericLiteral {
7128                value: index as f64,
7129                suffix: NumericSuffix::None,
7130                raw: index.to_string(),
7131                digest: None,
7132            },
7133        )))),
7134        computed: true,
7135        digest: None,
7136    })))
7137}
7138
7139/// Create an AST node for `fixed([point, [x, y]])`.
7140fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7141    // Create [x, y] array literal.
7142    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7143        position.x,
7144    )?))));
7145    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7146        position.y,
7147    )?))));
7148    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7149        elements: vec![x_literal, y_literal],
7150        digest: None,
7151        non_code_meta: Default::default(),
7152    })));
7153
7154    // Create [point, [x, y]] outer array.
7155    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7156        elements: vec![point_expr, point_array],
7157        digest: None,
7158        non_code_meta: Default::default(),
7159    })));
7160
7161    // Create fixed([...])
7162    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7163        ast::CallExpressionKw {
7164            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7165            unlabeled: Some(array_expr),
7166            arguments: Default::default(),
7167            digest: None,
7168            non_code_meta: Default::default(),
7169        },
7170    ))))
7171}
7172
7173/// Create an AST node for equalLength([line1, line2, ...])
7174pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7175    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7176        elements: line_exprs,
7177        digest: None,
7178        non_code_meta: Default::default(),
7179    })));
7180
7181    // Create equalLength([...])
7182    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7183        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7184        unlabeled: Some(array_expr),
7185        arguments: Default::default(),
7186        digest: None,
7187        non_code_meta: Default::default(),
7188    })))
7189}
7190
7191/// Create an AST node for equalRadius([seg1, seg2, ...])
7192pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7193    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7194        elements: segment_exprs,
7195        digest: None,
7196        non_code_meta: Default::default(),
7197    })));
7198
7199    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7200        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7201        unlabeled: Some(array_expr),
7202        arguments: Default::default(),
7203        digest: None,
7204        non_code_meta: Default::default(),
7205    })))
7206}
7207
7208/// Create an AST node for tangent([seg1, seg2])
7209pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7210    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7211        elements: vec![seg1_expr, seg2_expr],
7212        digest: None,
7213        non_code_meta: Default::default(),
7214    })));
7215
7216    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7217        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7218        unlabeled: Some(array_expr),
7219        arguments: Default::default(),
7220        digest: None,
7221        non_code_meta: Default::default(),
7222    })))
7223}
7224
7225/// Create an AST node for symmetric([input1, input2], axis = line)
7226pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7227    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7228        elements: input_exprs,
7229        digest: None,
7230        non_code_meta: Default::default(),
7231    })));
7232    let arguments = vec![ast::LabeledArg {
7233        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7234        arg: axis_expr,
7235    }];
7236
7237    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7238        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7239        unlabeled: Some(array_expr),
7240        arguments,
7241        digest: None,
7242        non_code_meta: Default::default(),
7243    })))
7244}
7245
7246/// Create an AST node for midpoint(segment, point = point)
7247pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7248    let arguments = vec![ast::LabeledArg {
7249        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7250        arg: point_expr,
7251    }];
7252
7253    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7254        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7255        unlabeled: Some(segment_expr),
7256        arguments,
7257        digest: None,
7258        non_code_meta: Default::default(),
7259    })))
7260}
7261
7262/// The source range to attach to a diagnostic for `error` in the top-level
7263/// module. Source ranges are ordered innermost first and may point into
7264/// imported modules, so prefer the innermost range that is in the top-level
7265/// module; otherwise fall back to the innermost range.
7266fn issue_source_range(error: &KclError) -> SourceRange {
7267    let source_ranges = error.source_ranges();
7268    source_ranges
7269        .iter()
7270        .find(|range| range.is_top_level_module())
7271        .or_else(|| source_ranges.first())
7272        .copied()
7273        .unwrap_or_else(SourceRange::synthetic)
7274}
7275
7276#[cfg(test)]
7277mod tests {
7278    use std::sync;
7279
7280    use super::*;
7281    use crate::engine::PlaneName;
7282    use crate::engine::engine_manager::EngineManager;
7283    use crate::execution::cache::SketchModeState;
7284    use crate::execution::cache::clear_mem_cache;
7285    use crate::execution::cache::read_old_memory;
7286    use crate::execution::cache::write_old_memory;
7287    use crate::front::Distance;
7288    use crate::front::Fixed;
7289    use crate::front::FixedPoint;
7290    use crate::front::Midpoint;
7291    use crate::front::Object;
7292    use crate::front::Plane;
7293    use crate::front::Sketch;
7294    use crate::front::Tangent;
7295    use crate::frontend::sketch::Vertical;
7296    use crate::pretty::NumericSuffix;
7297
7298    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7299        for object in &scene_graph.objects {
7300            if let ObjectKind::Sketch(_) = &object.kind {
7301                return Some(object);
7302            }
7303        }
7304        None
7305    }
7306
7307    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7308        for object in &scene_graph.objects {
7309            if let ObjectKind::Face(_) = &object.kind {
7310                return Some(object);
7311            }
7312        }
7313        None
7314    }
7315
7316    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7317        for object in &scene_graph.objects {
7318            if matches!(&object.kind, ObjectKind::Wall(_)) {
7319                return Some(object.id);
7320            }
7321        }
7322        None
7323    }
7324
7325    fn find_cap_object_id_with_solid_output_index(
7326        scene_graph: &SceneGraph,
7327        cap_kind: crate::frontend::api::CapKind,
7328        solid_output_index: usize,
7329    ) -> Option<ObjectId> {
7330        for object in &scene_graph.objects {
7331            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7332            {
7333                return Some(object.id);
7334            }
7335        }
7336        None
7337    }
7338
7339    #[test]
7340    fn issue_source_range_prefers_top_level_module() {
7341        use kcl_error::ModuleId;
7342
7343        let top = SourceRange::new(10, 20, ModuleId::default());
7344        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7345
7346        // Innermost frame is in an imported module; the diagnostic should
7347        // land on the innermost top-level range instead.
7348        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7349            "boom".to_owned(),
7350            vec![imported, top],
7351        ));
7352        assert_eq!(super::issue_source_range(&error), top);
7353
7354        // No top-level range at all: fall back to the innermost one.
7355        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7356        assert_eq!(super::issue_source_range(&error), imported);
7357
7358        // No ranges: synthetic.
7359        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7360        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7361    }
7362
7363    #[test]
7364    fn composite_constituent_sweeps_are_not_solid_outputs() {
7365        use kcl_api::artifact::ArtifactSweepMethod;
7366        use kcl_api::artifact::CompositeSolid;
7367        use kcl_api::artifact::CompositeSolidSubType;
7368        use kcl_api::artifact::Sweep;
7369        use kcl_api::artifact::SweepSubType;
7370
7371        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7372        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7373        let composite_id = ArtifactId::new(Uuid::new_v4());
7374        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7375        let sweep = |id| {
7376            Artifact::Sweep(Sweep {
7377                id,
7378                sub_type: SweepSubType::Extrusion,
7379                path_id: ArtifactId::new(Uuid::new_v4()),
7380                surface_ids: Vec::new(),
7381                edge_ids: Vec::new(),
7382                code_ref: code_ref.clone(),
7383                source_sweep_id: None,
7384                trajectory_id: None,
7385                method: ArtifactSweepMethod::New,
7386                consumed: false,
7387                pattern_ids: Vec::new(),
7388            })
7389        };
7390        let mut artifacts = IndexMap::from([
7391            (first_sweep_id, sweep(first_sweep_id)),
7392            (second_sweep_id, sweep(second_sweep_id)),
7393        ]);
7394
7395        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7396        assert_eq!(
7397            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7398            Some(0)
7399        );
7400        assert_eq!(
7401            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7402            Some(1)
7403        );
7404
7405        artifacts.insert(
7406            composite_id,
7407            Artifact::CompositeSolid(CompositeSolid {
7408                id: composite_id,
7409                consumed: false,
7410                sub_type: CompositeSolidSubType::Union,
7411                output_index: None,
7412                solid_ids: vec![first_sweep_id, second_sweep_id],
7413                tool_ids: Vec::new(),
7414                code_ref,
7415                composite_solid_id: None,
7416                pattern_ids: Vec::new(),
7417            }),
7418        );
7419        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7420        assert_eq!(
7421            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7422            None
7423        );
7424        assert_eq!(
7425            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7426            None
7427        );
7428    }
7429
7430    #[test]
7431    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7432        let source = "\
7433region001 = region(point = [0.1, 0.1], sketch = s)
7434extrude001 = extrude(region001, length = 5)
7435revolve001 = revolve(region001, axis = Y)
7436sweep001 = sweep(region001, path = path001)
7437loft001 = loft(region001)
7438not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7439";
7440
7441        let program = Program::parse(source).unwrap().0.unwrap();
7442
7443        assert_eq!(
7444            region_name_from_sweep_variable(&program.ast, "extrude001"),
7445            Some("region001".to_owned())
7446        );
7447        assert_eq!(
7448            region_name_from_sweep_variable(&program.ast, "revolve001"),
7449            Some("region001".to_owned())
7450        );
7451        assert_eq!(
7452            region_name_from_sweep_variable(&program.ast, "sweep001"),
7453            Some("region001".to_owned())
7454        );
7455        assert_eq!(
7456            region_name_from_sweep_variable(&program.ast, "loft001"),
7457            Some("region001".to_owned())
7458        );
7459        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7460    }
7461
7462    #[track_caller]
7463    fn expect_sketch(object: &Object) -> &Sketch {
7464        if let ObjectKind::Sketch(sketch) = &object.kind {
7465            sketch
7466        } else {
7467            panic!("Object is not a sketch: {:?}", object);
7468        }
7469    }
7470
7471    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7472        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7473        let ObjectKind::Segment {
7474            segment: Segment::Point(point),
7475        } = &point_object.kind
7476        else {
7477            panic!("Object is not a point segment: {point_object:?}");
7478        };
7479        point.position.clone()
7480    }
7481
7482    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7483        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7484        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7485    }
7486
7487    /// Build a millimeter-valued point expression for concise sketch edit test
7488    /// setup.
7489    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7490        Point2d {
7491            x: Expr::Var(Number {
7492                value: x,
7493                units: NumericSuffix::Mm,
7494            }),
7495            y: Expr::Var(Number {
7496                value: y,
7497                units: NumericSuffix::Mm,
7498            }),
7499        }
7500    }
7501
7502    /// Build a millimeter-valued numeric point for comparing solved scene graph
7503    /// positions.
7504    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7505        Point2d {
7506            x: Number {
7507                value: x,
7508                units: NumericSuffix::Mm,
7509            },
7510            y: Number {
7511                value: y,
7512                units: NumericSuffix::Mm,
7513            },
7514        }
7515    }
7516
7517    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7518        LineCtor {
7519            start: Point2d {
7520                x: Expr::Number(Number { value: start_x, units }),
7521                y: Expr::Number(Number { value: start_y, units }),
7522            },
7523            end: Point2d {
7524                x: Expr::Number(Number { value: end_x, units }),
7525                y: Expr::Number(Number { value: end_y, units }),
7526            },
7527            construction: None,
7528        }
7529    }
7530
7531    async fn create_sketch_with_single_line(
7532        frontend: &mut FrontendState,
7533        ctx: &ExecutorContext,
7534        mock_ctx: &ExecutorContext,
7535        version: Version,
7536    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7537        frontend.program = Program::empty();
7538
7539        let sketch_args = SketchCtor {
7540            on: Plane::Default(PlaneName::Xy),
7541        };
7542        let (_src_delta, _scene_delta, sketch_id) = frontend
7543            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7544            .await
7545            .unwrap();
7546
7547        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7548        let (source_delta, scene_graph_delta) = frontend
7549            .add_segment(mock_ctx, version, sketch_id, segment, None)
7550            .await
7551            .unwrap();
7552        let line_id = *scene_graph_delta
7553            .new_objects
7554            .last()
7555            .expect("Expected line object id to be created");
7556
7557        (sketch_id, line_id, source_delta, scene_graph_delta)
7558    }
7559
7560    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7561        frontend.program = program.clone();
7562        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7563        frontend.update_state_after_exec(outcome, true);
7564    }
7565
7566    #[test]
7567    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7568        for (source, expected_message) in [
7569            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7570            (
7571                "3'",
7572                "Error parsing KCL source after editing: unterminated string literal",
7573            ),
7574        ] {
7575            let err = parse_frontend_mutation_source(
7576                source,
7577                "Error parsing KCL source after editing",
7578                "No AST produced after editing",
7579            )
7580            .expect_err("expected invalid KCL source to fail");
7581            let message = err.error.message();
7582
7583            assert_eq!(message, expected_message);
7584            assert!(!message.contains("CompilationIssue"));
7585            assert!(!message.contains("KclErrorDetails"));
7586            assert!(!message.contains("source_range"));
7587        }
7588    }
7589
7590    #[tokio::test(flavor = "multi_thread")]
7591    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7592        let initial_source = "\
7593sketch(on = XY) {
7594  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7595  distance([line1.start, line1.end]) == 10
7596}
7597";
7598        let program = Program::parse(initial_source).unwrap().0.unwrap();
7599
7600        let mut frontend = FrontendState::new();
7601        let mock_ctx = ExecutorContext::new_mock(None).await;
7602        let version = Version(0);
7603
7604        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7605        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7606        let sketch_id = sketch_object.id;
7607        let sketch = expect_sketch(sketch_object);
7608        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7609
7610        for (value, expected_message) in [
7611            ("**", "Invalid constraint value: Unexpected token: *"),
7612            ("3'", "Invalid constraint value: unterminated string literal"),
7613        ] {
7614            let err = frontend
7615                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7616                .await
7617                .expect_err("expected invalid constraint expression to fail");
7618            let message = err.error.message();
7619
7620            assert_eq!(message, expected_message);
7621            assert!(!message.contains("CompilationIssue"));
7622            assert!(!message.contains("KclErrorDetails"));
7623            assert!(!message.contains("source_range"));
7624        }
7625
7626        mock_ctx.close().await;
7627    }
7628
7629    #[tokio::test(flavor = "multi_thread")]
7630    async fn test_failed_edit_constraint_value_does_not_update_program() {
7631        let initial_source = "\
7632sketch(on = XY) {
7633  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7634  distance([line1.start, line1.end]) == 10
7635}
7636";
7637        let program = Program::parse(initial_source).unwrap().0.unwrap();
7638        let original_source = program.original_file_contents.clone();
7639
7640        let mut frontend = FrontendState::new();
7641        let mock_ctx = ExecutorContext::new_mock(None).await;
7642        let version = Version(0);
7643
7644        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7645        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7646        let sketch_id = sketch_object.id;
7647        let sketch = expect_sketch(sketch_object);
7648        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7649
7650        frontend
7651            .edit_constraint_value(
7652                &mock_ctx,
7653                version,
7654                sketch_id,
7655                constraint_id,
7656                "unknownDistance".to_owned(),
7657            )
7658            .await
7659            .expect_err("expected invalid constraint value to fail execution");
7660
7661        assert_eq!(frontend.program.original_file_contents, original_source);
7662        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7663
7664        mock_ctx.close().await;
7665    }
7666
7667    #[tokio::test(flavor = "multi_thread")]
7668    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7669        let initial_source = "\
7670arr = [0]
7671sketch(on = XY) {
7672  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7673  distance([line1.start, line1.end]) == 10
7674}
7675";
7676        let program = Program::parse(initial_source).unwrap().0.unwrap();
7677
7678        let mut frontend = FrontendState::new();
7679        let mock_ctx = ExecutorContext::new_mock(None).await;
7680        let version = Version(0);
7681
7682        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7684        let sketch_id = sketch_object.id;
7685        let sketch = expect_sketch(sketch_object);
7686        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7687
7688        // In sketch mode execution, an out-of-bounds array index is an error.
7689        // It should not fall back to the first element of the array the way
7690        // plain mock execution does.
7691        let err = frontend
7692            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7693            .await
7694            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7695        let message = err.error.message();
7696        assert!(
7697            message.contains("The array doesn't have any item at index 5"),
7698            "unexpected error message: {message}"
7699        );
7700
7701        mock_ctx.close().await;
7702    }
7703
7704    #[tokio::test(flavor = "multi_thread")]
7705    async fn test_sketch_checkpoint_round_trip_restores_state() {
7706        let mut frontend = FrontendState::new();
7707        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7708        let mock_ctx = ExecutorContext::new_mock(None).await;
7709        let version = Version(0);
7710
7711        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7712            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7713
7714        let expected_source = source_delta.text.clone();
7715        let expected_scene_graph = frontend.scene_graph.clone();
7716        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7717        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7718
7719        let checkpoint_id = frontend
7720            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7721            .await
7722            .unwrap();
7723
7724        let edited_segments = vec![ExistingSegmentCtor {
7725            id: line_id,
7726            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7727        }];
7728        let (edited_source, _edited_scene) = frontend
7729            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7730            .await
7731            .unwrap();
7732        assert_ne!(edited_source.text, expected_source);
7733
7734        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7735
7736        assert_eq!(restored.source_delta.text, expected_source);
7737        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7738        assert!(restored.scene_graph_delta.invalidates_ids);
7739        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7740        assert_eq!(frontend.scene_graph, expected_scene_graph);
7741        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7742
7743        ctx.close().await;
7744    }
7745
7746    #[tokio::test(flavor = "multi_thread")]
7747    async fn test_sketch_checkpoints_prune_oldest_entries() {
7748        let mut frontend = FrontendState::new();
7749        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7750        let mock_ctx = ExecutorContext::new_mock(None).await;
7751        let version = Version(0);
7752
7753        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7754            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7755
7756        let mut checkpoint_ids = Vec::new();
7757        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7758            checkpoint_ids.push(
7759                frontend
7760                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7761                    .await
7762                    .unwrap(),
7763            );
7764        }
7765
7766        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7767        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7768
7769        let oldest_retained = checkpoint_ids[3];
7770        assert_eq!(
7771            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7772            Some(oldest_retained)
7773        );
7774
7775        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7776        assert!(evicted_restore.is_err());
7777        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7778
7779        frontend
7780            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7781            .await
7782            .unwrap();
7783
7784        ctx.close().await;
7785    }
7786
7787    #[tokio::test(flavor = "multi_thread")]
7788    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7789        let mut frontend = FrontendState::new();
7790        let missing_checkpoint = SketchCheckpointId::new(999);
7791
7792        let err = frontend
7793            .restore_sketch_checkpoint(missing_checkpoint)
7794            .await
7795            .expect_err("Expected restore to fail for missing checkpoint");
7796
7797        assert!(err.msg.contains("Sketch checkpoint not found"));
7798    }
7799
7800    #[tokio::test(flavor = "multi_thread")]
7801    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7802        let mut frontend = FrontendState::new();
7803        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7804        let mock_ctx = ExecutorContext::new_mock(None).await;
7805        let version = Version(0);
7806
7807        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7808            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7809
7810        let checkpoint_a = frontend
7811            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7812            .await
7813            .unwrap();
7814        let checkpoint_b = frontend
7815            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7816            .await
7817            .unwrap();
7818        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7819
7820        frontend.clear_sketch_checkpoints();
7821        assert!(frontend.sketch_checkpoints.is_empty());
7822        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7823        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7824
7825        ctx.close().await;
7826    }
7827
7828    #[tokio::test(flavor = "multi_thread")]
7829    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7830        let mut frontend = FrontendState::new();
7831        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7832        let mock_ctx = ExecutorContext::new_mock(None).await;
7833        let version = Version(0);
7834
7835        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7836            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7837        let old_source = source_delta.text.clone();
7838        let old_checkpoint = frontend
7839            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7840            .await
7841            .unwrap();
7842        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7843
7844        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7845            .unwrap()
7846            .0
7847            .unwrap();
7848
7849        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7850        let SetProgramOutcome::Success {
7851            checkpoint_id: Some(new_checkpoint),
7852            ..
7853        } = result
7854        else {
7855            panic!("Expected Success with a fresh checkpoint baseline");
7856        };
7857
7858        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7859
7860        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7861        assert_eq!(old_restore.source_delta.text, old_source);
7862
7863        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7864        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7865
7866        ctx.close().await;
7867    }
7868
7869    #[tokio::test(flavor = "multi_thread")]
7870    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7871        let mut frontend = FrontendState::new();
7872        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7873        let mock_ctx = ExecutorContext::new_mock(None).await;
7874        let version = Version(0);
7875
7876        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7877            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7878        let old_checkpoint = frontend
7879            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7880            .await
7881            .unwrap();
7882        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7883
7884        let failing_program = Program::parse(
7885            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7886        )
7887        .unwrap()
7888        .0
7889        .unwrap();
7890
7891        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7892        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7893        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7894        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7895
7896        ctx.close().await;
7897    }
7898
7899    #[tokio::test(flavor = "multi_thread")]
7900    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7901        let mut frontend = FrontendState::new();
7902        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7903
7904        let program = Program::parse(
7905            "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",
7906        )
7907        .unwrap()
7908        .0
7909        .unwrap();
7910        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7911        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7912            panic!("Expected successful baseline program execution");
7913        };
7914
7915        clear_mem_cache().await;
7916        assert!(read_old_memory().await.is_none());
7917
7918        let checkpoint_without_mock_memory = frontend
7919            .create_sketch_checkpoint((*exec_outcome).clone())
7920            .await
7921            .unwrap();
7922
7923        write_old_memory(SketchModeState::new_for_tests()).await;
7924        assert!(read_old_memory().await.is_some());
7925
7926        let checkpoint_with_mock_memory = frontend
7927            .create_sketch_checkpoint((*exec_outcome).clone())
7928            .await
7929            .unwrap();
7930
7931        clear_mem_cache().await;
7932        assert!(read_old_memory().await.is_none());
7933
7934        frontend
7935            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7936            .await
7937            .unwrap();
7938        assert!(read_old_memory().await.is_some());
7939
7940        frontend
7941            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7942            .await
7943            .unwrap();
7944        assert!(read_old_memory().await.is_none());
7945
7946        ctx.close().await;
7947    }
7948
7949    #[tokio::test(flavor = "multi_thread")]
7950    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7951        let source = "\
7952sketch(on = XY) {
7953  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7954}
7955
7956bad = missing_name
7957";
7958        let program = Program::parse(source).unwrap().0.unwrap();
7959
7960        let mut frontend = FrontendState::new();
7961
7962        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7963        let mock_ctx = ExecutorContext::new_mock(None).await;
7964        let version = Version(0);
7965        let project_id = ProjectId(0);
7966        let file_id = FileId(0);
7967
7968        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7969            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7970        };
7971
7972        let sketch_id = frontend
7973            .scene_graph
7974            .objects
7975            .iter()
7976            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7977            .expect("Expected sketch object from errored hack_set_program");
7978
7979        frontend
7980            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7981            .await
7982            .unwrap();
7983
7984        ctx.close().await;
7985        mock_ctx.close().await;
7986    }
7987
7988    #[tokio::test(flavor = "multi_thread")]
7989    async fn test_new_sketch_add_point_edit_point() {
7990        let program = Program::empty();
7991
7992        let mut frontend = FrontendState::new();
7993        frontend.program = program;
7994
7995        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7996        let mock_ctx = ExecutorContext::new_mock(None).await;
7997        let version = Version(0);
7998
7999        let sketch_args = SketchCtor {
8000            on: Plane::Default(PlaneName::Xy),
8001        };
8002        let (_src_delta, scene_delta, sketch_id) = frontend
8003            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8004            .await
8005            .unwrap();
8006        assert_eq!(sketch_id, ObjectId(1));
8007        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8008        let sketch_object = &scene_delta.new_graph.objects[1];
8009        assert_eq!(sketch_object.id, ObjectId(1));
8010        assert_eq!(
8011            sketch_object.kind,
8012            ObjectKind::Sketch(Sketch {
8013                args: SketchCtor {
8014                    on: Plane::Default(PlaneName::Xy)
8015                },
8016                plane: ObjectId(0),
8017                segments: vec![],
8018                constraints: vec![],
8019            })
8020        );
8021        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8022
8023        let point_ctor = PointCtor {
8024            position: Point2d {
8025                x: Expr::Number(Number {
8026                    value: 1.0,
8027                    units: NumericSuffix::Inch,
8028                }),
8029                y: Expr::Number(Number {
8030                    value: 2.0,
8031                    units: NumericSuffix::Inch,
8032                }),
8033            },
8034        };
8035        let segment = SegmentCtor::Point(point_ctor);
8036        let (src_delta, scene_delta) = frontend
8037            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8038            .await
8039            .unwrap();
8040        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8041        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8042        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8043        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8044            assert_eq!(scene_object.id.0, i);
8045        }
8046
8047        let point_id = *scene_delta.new_objects.last().unwrap();
8048
8049        let point_ctor = PointCtor {
8050            position: Point2d {
8051                x: Expr::Number(Number {
8052                    value: 3.0,
8053                    units: NumericSuffix::Inch,
8054                }),
8055                y: Expr::Number(Number {
8056                    value: 4.0,
8057                    units: NumericSuffix::Inch,
8058                }),
8059            },
8060        };
8061        let segments = vec![ExistingSegmentCtor {
8062            id: point_id,
8063            ctor: SegmentCtor::Point(point_ctor),
8064        }];
8065        let (src_delta, scene_delta) = frontend
8066            .edit_segments(&mock_ctx, version, sketch_id, segments)
8067            .await
8068            .unwrap();
8069        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8070        assert_eq!(scene_delta.new_objects, vec![]);
8071        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8072
8073        ctx.close().await;
8074        mock_ctx.close().await;
8075    }
8076
8077    #[tokio::test(flavor = "multi_thread")]
8078    async fn test_new_sketch_add_line_edit_line() {
8079        let program = Program::empty();
8080
8081        let mut frontend = FrontendState::new();
8082        frontend.program = program;
8083
8084        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8085        let mock_ctx = ExecutorContext::new_mock(None).await;
8086        let version = Version(0);
8087
8088        let sketch_args = SketchCtor {
8089            on: Plane::Default(PlaneName::Xy),
8090        };
8091        let (_src_delta, scene_delta, sketch_id) = frontend
8092            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8093            .await
8094            .unwrap();
8095        assert_eq!(sketch_id, ObjectId(1));
8096        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8097        let sketch_object = &scene_delta.new_graph.objects[1];
8098        assert_eq!(sketch_object.id, ObjectId(1));
8099        assert_eq!(
8100            sketch_object.kind,
8101            ObjectKind::Sketch(Sketch {
8102                args: SketchCtor {
8103                    on: Plane::Default(PlaneName::Xy)
8104                },
8105                plane: ObjectId(0),
8106                segments: vec![],
8107                constraints: vec![],
8108            })
8109        );
8110        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8111
8112        let line_ctor = LineCtor {
8113            start: Point2d {
8114                x: Expr::Number(Number {
8115                    value: 0.0,
8116                    units: NumericSuffix::Mm,
8117                }),
8118                y: Expr::Number(Number {
8119                    value: 0.0,
8120                    units: NumericSuffix::Mm,
8121                }),
8122            },
8123            end: Point2d {
8124                x: Expr::Number(Number {
8125                    value: 10.0,
8126                    units: NumericSuffix::Mm,
8127                }),
8128                y: Expr::Number(Number {
8129                    value: 10.0,
8130                    units: NumericSuffix::Mm,
8131                }),
8132            },
8133            construction: None,
8134        };
8135        let segment = SegmentCtor::Line(line_ctor);
8136        let (src_delta, scene_delta) = frontend
8137            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8138            .await
8139            .unwrap();
8140        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8141        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8142        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8143        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8144            assert_eq!(scene_object.id.0, i);
8145        }
8146
8147        // The new objects are the end points and then the line.
8148        let line = *scene_delta.new_objects.last().unwrap();
8149
8150        let line_ctor = LineCtor {
8151            start: Point2d {
8152                x: Expr::Number(Number {
8153                    value: 1.0,
8154                    units: NumericSuffix::Mm,
8155                }),
8156                y: Expr::Number(Number {
8157                    value: 2.0,
8158                    units: NumericSuffix::Mm,
8159                }),
8160            },
8161            end: Point2d {
8162                x: Expr::Number(Number {
8163                    value: 13.0,
8164                    units: NumericSuffix::Mm,
8165                }),
8166                y: Expr::Number(Number {
8167                    value: 14.0,
8168                    units: NumericSuffix::Mm,
8169                }),
8170            },
8171            construction: None,
8172        };
8173        let segments = vec![ExistingSegmentCtor {
8174            id: line,
8175            ctor: SegmentCtor::Line(line_ctor),
8176        }];
8177        let (src_delta, scene_delta) = frontend
8178            .edit_segments(&mock_ctx, version, sketch_id, segments)
8179            .await
8180            .unwrap();
8181        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8182        assert_eq!(scene_delta.new_objects, vec![]);
8183        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8184
8185        ctx.close().await;
8186        mock_ctx.close().await;
8187    }
8188
8189    #[tokio::test(flavor = "multi_thread")]
8190    async fn test_new_sketch_add_arc_edit_arc() {
8191        let program = Program::empty();
8192
8193        let mut frontend = FrontendState::new();
8194        frontend.program = program;
8195
8196        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8197        let mock_ctx = ExecutorContext::new_mock(None).await;
8198        let version = Version(0);
8199
8200        let sketch_args = SketchCtor {
8201            on: Plane::Default(PlaneName::Xy),
8202        };
8203        let (_src_delta, scene_delta, sketch_id) = frontend
8204            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8205            .await
8206            .unwrap();
8207        assert_eq!(sketch_id, ObjectId(1));
8208        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8209        let sketch_object = &scene_delta.new_graph.objects[1];
8210        assert_eq!(sketch_object.id, ObjectId(1));
8211        assert_eq!(
8212            sketch_object.kind,
8213            ObjectKind::Sketch(Sketch {
8214                args: SketchCtor {
8215                    on: Plane::Default(PlaneName::Xy),
8216                },
8217                plane: ObjectId(0),
8218                segments: vec![],
8219                constraints: vec![],
8220            })
8221        );
8222        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8223
8224        let arc_ctor = ArcCtor {
8225            start: Point2d {
8226                x: Expr::Var(Number {
8227                    value: 0.0,
8228                    units: NumericSuffix::Mm,
8229                }),
8230                y: Expr::Var(Number {
8231                    value: 0.0,
8232                    units: NumericSuffix::Mm,
8233                }),
8234            },
8235            end: Point2d {
8236                x: Expr::Var(Number {
8237                    value: 10.0,
8238                    units: NumericSuffix::Mm,
8239                }),
8240                y: Expr::Var(Number {
8241                    value: 10.0,
8242                    units: NumericSuffix::Mm,
8243                }),
8244            },
8245            center: Point2d {
8246                x: Expr::Var(Number {
8247                    value: 10.0,
8248                    units: NumericSuffix::Mm,
8249                }),
8250                y: Expr::Var(Number {
8251                    value: 0.0,
8252                    units: NumericSuffix::Mm,
8253                }),
8254            },
8255            direction: None,
8256            construction: None,
8257        };
8258        let segment = SegmentCtor::Arc(arc_ctor);
8259        let (src_delta, scene_delta) = frontend
8260            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8261            .await
8262            .unwrap();
8263        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8264        assert_eq!(
8265            scene_delta.new_objects,
8266            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8267        );
8268        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8269            assert_eq!(scene_object.id.0, i);
8270        }
8271        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8272
8273        // The new objects are the end points, the center, and then the arc.
8274        let arc = *scene_delta.new_objects.last().unwrap();
8275
8276        let arc_ctor = ArcCtor {
8277            start: Point2d {
8278                x: Expr::Var(Number {
8279                    value: 1.0,
8280                    units: NumericSuffix::Mm,
8281                }),
8282                y: Expr::Var(Number {
8283                    value: 2.0,
8284                    units: NumericSuffix::Mm,
8285                }),
8286            },
8287            end: Point2d {
8288                x: Expr::Var(Number {
8289                    value: 13.0,
8290                    units: NumericSuffix::Mm,
8291                }),
8292                y: Expr::Var(Number {
8293                    value: 14.0,
8294                    units: NumericSuffix::Mm,
8295                }),
8296            },
8297            center: Point2d {
8298                x: Expr::Var(Number {
8299                    value: 13.0,
8300                    units: NumericSuffix::Mm,
8301                }),
8302                y: Expr::Var(Number {
8303                    value: 2.0,
8304                    units: NumericSuffix::Mm,
8305                }),
8306            },
8307            direction: None,
8308            construction: None,
8309        };
8310        let segments = vec![ExistingSegmentCtor {
8311            id: arc,
8312            ctor: SegmentCtor::Arc(arc_ctor),
8313        }];
8314        let (src_delta, scene_delta) = frontend
8315            .edit_segments(&mock_ctx, version, sketch_id, segments)
8316            .await
8317            .unwrap();
8318        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8319        assert_eq!(scene_delta.new_objects, vec![]);
8320        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8321
8322        ctx.close().await;
8323        mock_ctx.close().await;
8324    }
8325
8326    #[tokio::test(flavor = "multi_thread")]
8327    async fn test_new_sketch_add_circle_edit_circle() {
8328        let program = Program::empty();
8329
8330        let mut frontend = FrontendState::new();
8331        frontend.program = program;
8332
8333        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8334        let mock_ctx = ExecutorContext::new_mock(None).await;
8335        let version = Version(0);
8336
8337        let sketch_args = SketchCtor {
8338            on: Plane::Default(PlaneName::Xy),
8339        };
8340        let (_src_delta, _scene_delta, sketch_id) = frontend
8341            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8342            .await
8343            .unwrap();
8344
8345        // Add a circle segment.
8346        let circle_ctor = CircleCtor {
8347            start: Point2d {
8348                x: Expr::Var(Number {
8349                    value: 5.0,
8350                    units: NumericSuffix::Mm,
8351                }),
8352                y: Expr::Var(Number {
8353                    value: 0.0,
8354                    units: NumericSuffix::Mm,
8355                }),
8356            },
8357            center: Point2d {
8358                x: Expr::Var(Number {
8359                    value: 0.0,
8360                    units: NumericSuffix::Mm,
8361                }),
8362                y: Expr::Var(Number {
8363                    value: 0.0,
8364                    units: NumericSuffix::Mm,
8365                }),
8366            },
8367            construction: None,
8368        };
8369        let segment = SegmentCtor::Circle(circle_ctor);
8370        let (src_delta, scene_delta) = frontend
8371            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8372            .await
8373            .unwrap();
8374        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8375        // The new objects are start, center, and then the circle segment.
8376        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8377        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8378
8379        let circle = *scene_delta.new_objects.last().unwrap();
8380
8381        // Edit the circle segment.
8382        let circle_ctor = CircleCtor {
8383            start: Point2d {
8384                x: Expr::Var(Number {
8385                    value: 10.0,
8386                    units: NumericSuffix::Mm,
8387                }),
8388                y: Expr::Var(Number {
8389                    value: 0.0,
8390                    units: NumericSuffix::Mm,
8391                }),
8392            },
8393            center: Point2d {
8394                x: Expr::Var(Number {
8395                    value: 3.0,
8396                    units: NumericSuffix::Mm,
8397                }),
8398                y: Expr::Var(Number {
8399                    value: 4.0,
8400                    units: NumericSuffix::Mm,
8401                }),
8402            },
8403            construction: None,
8404        };
8405        let segments = vec![ExistingSegmentCtor {
8406            id: circle,
8407            ctor: SegmentCtor::Circle(circle_ctor),
8408        }];
8409        let (src_delta, scene_delta) = frontend
8410            .edit_segments(&mock_ctx, version, sketch_id, segments)
8411            .await
8412            .unwrap();
8413        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8414        assert_eq!(scene_delta.new_objects, vec![]);
8415        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8416
8417        ctx.close().await;
8418        mock_ctx.close().await;
8419    }
8420
8421    #[tokio::test(flavor = "multi_thread")]
8422    async fn test_delete_circle() {
8423        let initial_source = "sketch001 = sketch(on = XY) {
8424  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8425}
8426";
8427
8428        let program = Program::parse(initial_source).unwrap().0.unwrap();
8429        let mut frontend = FrontendState::new();
8430
8431        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8432        let mock_ctx = ExecutorContext::new_mock(None).await;
8433        let version = Version(0);
8434
8435        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8436        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8437        let sketch_id = sketch_object.id;
8438        let sketch = expect_sketch(sketch_object);
8439
8440        // The sketch should have 3 segments: start point, center point, and the circle.
8441        assert_eq!(sketch.segments.len(), 3);
8442        let circle_id = sketch.segments[2];
8443
8444        // Delete the circle.
8445        let (src_delta, scene_delta) = frontend
8446            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8447            .await
8448            .unwrap();
8449        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8450        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8451        let new_sketch = expect_sketch(new_sketch_object);
8452        assert_eq!(new_sketch.segments.len(), 0);
8453
8454        ctx.close().await;
8455        mock_ctx.close().await;
8456    }
8457
8458    #[tokio::test(flavor = "multi_thread")]
8459    async fn test_edit_circle_via_point() {
8460        let initial_source = "sketch001 = sketch(on = XY) {
8461  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8462}
8463";
8464
8465        let program = Program::parse(initial_source).unwrap().0.unwrap();
8466        let mut frontend = FrontendState::new();
8467
8468        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8469        let mock_ctx = ExecutorContext::new_mock(None).await;
8470        let version = Version(0);
8471
8472        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8473        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8474        let sketch_id = sketch_object.id;
8475        let sketch = expect_sketch(sketch_object);
8476
8477        // Find the circle segment and its start point.
8478        let circle_id = sketch
8479            .segments
8480            .iter()
8481            .copied()
8482            .find(|seg_id| {
8483                matches!(
8484                    &frontend.scene_graph.objects[seg_id.0].kind,
8485                    ObjectKind::Segment {
8486                        segment: Segment::Circle(_)
8487                    }
8488                )
8489            })
8490            .expect("Expected a circle segment in sketch");
8491        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8492        let ObjectKind::Segment {
8493            segment: Segment::Circle(circle),
8494        } = &circle_object.kind
8495        else {
8496            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8497        };
8498        let start_point_id = circle.start;
8499
8500        // Edit the start point via SegmentCtor::Point.
8501        let segments = vec![ExistingSegmentCtor {
8502            id: start_point_id,
8503            ctor: SegmentCtor::Point(PointCtor {
8504                position: Point2d {
8505                    x: Expr::Var(Number {
8506                        value: 7.0,
8507                        units: NumericSuffix::Mm,
8508                    }),
8509                    y: Expr::Var(Number {
8510                        value: 1.0,
8511                        units: NumericSuffix::Mm,
8512                    }),
8513                },
8514            }),
8515        }];
8516        let (src_delta, _scene_delta) = frontend
8517            .edit_segments(&mock_ctx, version, sketch_id, segments)
8518            .await
8519            .unwrap();
8520        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8521
8522        ctx.close().await;
8523        mock_ctx.close().await;
8524    }
8525
8526    #[tokio::test(flavor = "multi_thread")]
8527    async fn test_add_line_when_sketch_block_uses_variable() {
8528        let initial_source = "s = sketch(on = XY) {}
8529";
8530
8531        let program = Program::parse(initial_source).unwrap().0.unwrap();
8532
8533        let mut frontend = FrontendState::new();
8534
8535        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8536        let mock_ctx = ExecutorContext::new_mock(None).await;
8537        let version = Version(0);
8538
8539        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8540        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8541        let sketch_id = sketch_object.id;
8542
8543        let line_ctor = LineCtor {
8544            start: Point2d {
8545                x: Expr::Number(Number {
8546                    value: 0.0,
8547                    units: NumericSuffix::Mm,
8548                }),
8549                y: Expr::Number(Number {
8550                    value: 0.0,
8551                    units: NumericSuffix::Mm,
8552                }),
8553            },
8554            end: Point2d {
8555                x: Expr::Number(Number {
8556                    value: 10.0,
8557                    units: NumericSuffix::Mm,
8558                }),
8559                y: Expr::Number(Number {
8560                    value: 10.0,
8561                    units: NumericSuffix::Mm,
8562                }),
8563            },
8564            construction: None,
8565        };
8566        let segment = SegmentCtor::Line(line_ctor);
8567        let (src_delta, scene_delta) = frontend
8568            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8569            .await
8570            .unwrap();
8571        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8572        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8573        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8574
8575        ctx.close().await;
8576        mock_ctx.close().await;
8577    }
8578
8579    #[tokio::test(flavor = "multi_thread")]
8580    async fn test_new_sketch_add_line_delete_sketch() {
8581        let program = Program::empty();
8582
8583        let mut frontend = FrontendState::new();
8584        frontend.program = program;
8585
8586        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8587        let mock_ctx = ExecutorContext::new_mock(None).await;
8588        let version = Version(0);
8589
8590        let sketch_args = SketchCtor {
8591            on: Plane::Default(PlaneName::Xy),
8592        };
8593        let (_src_delta, scene_delta, sketch_id) = frontend
8594            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8595            .await
8596            .unwrap();
8597        assert_eq!(sketch_id, ObjectId(1));
8598        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8599        let sketch_object = &scene_delta.new_graph.objects[1];
8600        assert_eq!(sketch_object.id, ObjectId(1));
8601        assert_eq!(
8602            sketch_object.kind,
8603            ObjectKind::Sketch(Sketch {
8604                args: SketchCtor {
8605                    on: Plane::Default(PlaneName::Xy)
8606                },
8607                plane: ObjectId(0),
8608                segments: vec![],
8609                constraints: vec![],
8610            })
8611        );
8612        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8613
8614        let line_ctor = LineCtor {
8615            start: Point2d {
8616                x: Expr::Number(Number {
8617                    value: 0.0,
8618                    units: NumericSuffix::Mm,
8619                }),
8620                y: Expr::Number(Number {
8621                    value: 0.0,
8622                    units: NumericSuffix::Mm,
8623                }),
8624            },
8625            end: Point2d {
8626                x: Expr::Number(Number {
8627                    value: 10.0,
8628                    units: NumericSuffix::Mm,
8629                }),
8630                y: Expr::Number(Number {
8631                    value: 10.0,
8632                    units: NumericSuffix::Mm,
8633                }),
8634            },
8635            construction: None,
8636        };
8637        let segment = SegmentCtor::Line(line_ctor);
8638        let (src_delta, scene_delta) = frontend
8639            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8640            .await
8641            .unwrap();
8642        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8643        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8644
8645        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8646        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8647        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8648
8649        ctx.close().await;
8650        mock_ctx.close().await;
8651    }
8652
8653    #[tokio::test(flavor = "multi_thread")]
8654    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8655        let initial_source = "s = sketch(on = XY) {}
8656";
8657
8658        let program = Program::parse(initial_source).unwrap().0.unwrap();
8659
8660        let mut frontend = FrontendState::new();
8661
8662        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8663        let version = Version(0);
8664
8665        frontend.hack_set_program(&ctx, program).await.unwrap();
8666        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8667        let sketch_id = sketch_object.id;
8668
8669        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8670        insta::assert_snapshot!(
8671            "test_delete_sketch_when_sketch_block_uses_variable",
8672            src_delta.text.as_str()
8673        );
8674        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8675
8676        ctx.close().await;
8677    }
8678
8679    #[tokio::test(flavor = "multi_thread")]
8680    async fn test_delete_sketch_after_comment() {
8681        let initial_source = "sketch001 = sketch(on = XZ) {
8682}
8683";
8684
8685        let program = Program::parse(initial_source).unwrap().0.unwrap();
8686        let mut frontend = FrontendState::new();
8687
8688        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8689        let version = Version(0);
8690
8691        frontend.hack_set_program(&ctx, program).await.unwrap();
8692        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8693        let sketch_id = sketch_object.id;
8694        let original_source = sketch_object.source.clone();
8695
8696        let commented_source = "// test 1
8697sketch001 = sketch(on = XZ) {
8698}
8699";
8700        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8701        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8702
8703        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8704        assert_eq!(cached_sketch_object.source, original_source);
8705
8706        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8707        assert!(
8708            !src_delta.text.contains("sketch001"),
8709            "sketch was not deleted: {}",
8710            src_delta.text
8711        );
8712        // The leading line comment must survive deletion.
8713        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8714        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8715
8716        ctx.close().await;
8717    }
8718
8719    #[tokio::test(flavor = "multi_thread")]
8720    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8721        let initial_source = "sketch001 = sketch(on = XZ) {
8722}
8723foo = 1
8724";
8725
8726        let program = Program::parse(initial_source).unwrap().0.unwrap();
8727        let mut frontend = FrontendState::new();
8728
8729        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8730        let version = Version(0);
8731
8732        frontend.hack_set_program(&ctx, program).await.unwrap();
8733        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8734        let sketch_id = sketch_object.id;
8735
8736        let commented_source = "// keep me
8737sketch001 = sketch(on = XZ) {
8738}
8739foo = 1
8740";
8741        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8742        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8743
8744        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8745        // The leading comment should remain, now attached to the following body item.
8746        insta::assert_snapshot!(
8747            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8748            src_delta.text.as_str()
8749        );
8750
8751        ctx.close().await;
8752    }
8753
8754    #[tokio::test(flavor = "multi_thread")]
8755    async fn test_delete_segment_preserves_pre_comment() {
8756        let initial_source = "\
8757sketch(on = XY) {
8758  point(at = [var 1, var 2])
8759  // describe the middle point
8760  point(at = [var 3, var 4])
8761  point(at = [var 5, var 6])
8762}
8763";
8764
8765        let program = Program::parse(initial_source).unwrap().0.unwrap();
8766        let mut frontend = FrontendState::new();
8767
8768        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8769        let mock_ctx = ExecutorContext::new_mock(None).await;
8770        let version = Version(0);
8771
8772        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8773        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8774        let sketch_id = sketch_object.id;
8775        let sketch = expect_sketch(sketch_object);
8776
8777        let middle_point_id = *sketch.segments.get(1).unwrap();
8778
8779        let (src_delta, _scene_delta) = frontend
8780            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8781            .await
8782            .unwrap();
8783        // The line comment on the line above the deleted point must be preserved.
8784        // It is reattached to the next surviving body item.
8785        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8786
8787        ctx.close().await;
8788        mock_ctx.close().await;
8789    }
8790
8791    #[tokio::test(flavor = "multi_thread")]
8792    async fn test_delete_last_segment_preserves_pre_comment() {
8793        let initial_source = "\
8794sketch(on = XY) {
8795  point(at = [var 1, var 2])
8796  // describe the trailing point
8797  point(at = [var 3, var 4])
8798}
8799";
8800
8801        let program = Program::parse(initial_source).unwrap().0.unwrap();
8802        let mut frontend = FrontendState::new();
8803
8804        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8805        let mock_ctx = ExecutorContext::new_mock(None).await;
8806        let version = Version(0);
8807
8808        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8809        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8810        let sketch_id = sketch_object.id;
8811        let sketch = expect_sketch(sketch_object);
8812
8813        let last_point_id = *sketch.segments.last().unwrap();
8814
8815        let (src_delta, _scene_delta) = frontend
8816            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8817            .await
8818            .unwrap();
8819        // No following item to attach to; the comment is kept inside the sketch
8820        // block as trailing non-code metadata so the user does not lose it.
8821        insta::assert_snapshot!(
8822            "test_delete_last_segment_preserves_pre_comment",
8823            src_delta.text.as_str()
8824        );
8825
8826        ctx.close().await;
8827        mock_ctx.close().await;
8828    }
8829
8830    #[tokio::test(flavor = "multi_thread")]
8831    async fn test_delete_segment_drops_inline_trailing_comment() {
8832        let initial_source = "\
8833sketch(on = XY) {
8834  point(at = [var 1, var 2])
8835  point(at = [var 3, var 4]) // same-line note that gets dropped
8836  point(at = [var 5, var 6])
8837}
8838";
8839
8840        let program = Program::parse(initial_source).unwrap().0.unwrap();
8841        let mut frontend = FrontendState::new();
8842
8843        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8844        let mock_ctx = ExecutorContext::new_mock(None).await;
8845        let version = Version(0);
8846
8847        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8848        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8849        let sketch_id = sketch_object.id;
8850        let sketch = expect_sketch(sketch_object);
8851
8852        let middle_point_id = *sketch.segments.get(1).unwrap();
8853
8854        let (src_delta, _scene_delta) = frontend
8855            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8856            .await
8857            .unwrap();
8858        // The same-line trailing comment is removed along with the deleted code.
8859        assert!(
8860            !src_delta.text.contains("same-line note"),
8861            "inline comment should have been removed: {}",
8862            src_delta.text
8863        );
8864
8865        ctx.close().await;
8866        mock_ctx.close().await;
8867    }
8868
8869    #[tokio::test(flavor = "multi_thread")]
8870    async fn test_delete_segments_preserves_block_comments_across_positions() {
8871        // One test exercising several `delete_body_item_preserving_pre_comments`
8872        // branches at once with `/* ... */` block comments:
8873        //   - first point: leading block comment must migrate to the next item.
8874        //   - first point: same-line trailing block comment must be dropped.
8875        //   - middle point: leading block comment must stay attached after migration.
8876        //   - last point: leading block comment, with no surviving next item,
8877        //     must be converted into a trailing NonCodeNode.
8878        let initial_source = "\
8879sketch(on = XY) {
8880  /* above first - moves to middle */
8881  point(at = [var 1, var 2]) /* same-line on first - dropped */
8882  /* above middle - stays */
8883  point(at = [var 3, var 4])
8884  /* above last - moves to trailing meta */
8885  point(at = [var 5, var 6])
8886}
8887";
8888
8889        let program = Program::parse(initial_source).unwrap().0.unwrap();
8890        let mut frontend = FrontendState::new();
8891
8892        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8893        let mock_ctx = ExecutorContext::new_mock(None).await;
8894        let version = Version(0);
8895
8896        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8897        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8898        let sketch_id = sketch_object.id;
8899        let sketch = expect_sketch(sketch_object);
8900
8901        let first_point_id = *sketch.segments.first().unwrap();
8902        let last_point_id = *sketch.segments.last().unwrap();
8903
8904        let (src_delta, _scene_delta) = frontend
8905            .delete_objects(
8906                &mock_ctx,
8907                version,
8908                sketch_id,
8909                Vec::new(),
8910                vec![first_point_id, last_point_id],
8911            )
8912            .await
8913            .unwrap();
8914        insta::assert_snapshot!(
8915            "test_delete_segments_preserves_block_comments_across_positions",
8916            src_delta.text.as_str()
8917        );
8918
8919        ctx.close().await;
8920        mock_ctx.close().await;
8921    }
8922
8923    #[tokio::test(flavor = "multi_thread")]
8924    async fn test_edit_line_when_editing_its_start_point() {
8925        let initial_source = "\
8926sketch(on = XY) {
8927  line(start = [var 1, var 2], end = [var 3, var 4])
8928}
8929";
8930
8931        let program = Program::parse(initial_source).unwrap().0.unwrap();
8932
8933        let mut frontend = FrontendState::new();
8934
8935        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8936        let mock_ctx = ExecutorContext::new_mock(None).await;
8937        let version = Version(0);
8938
8939        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8940        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8941        let sketch_id = sketch_object.id;
8942        let sketch = expect_sketch(sketch_object);
8943
8944        let point_id = *sketch.segments.first().unwrap();
8945
8946        let point_ctor = PointCtor {
8947            position: Point2d {
8948                x: Expr::Var(Number {
8949                    value: 5.0,
8950                    units: NumericSuffix::Inch,
8951                }),
8952                y: Expr::Var(Number {
8953                    value: 6.0,
8954                    units: NumericSuffix::Inch,
8955                }),
8956            },
8957        };
8958        let segments = vec![ExistingSegmentCtor {
8959            id: point_id,
8960            ctor: SegmentCtor::Point(point_ctor),
8961        }];
8962        let (src_delta, scene_delta) = frontend
8963            .edit_segments(&mock_ctx, version, sketch_id, segments)
8964            .await
8965            .unwrap();
8966        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8967        assert_eq!(scene_delta.new_objects, vec![]);
8968        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8969
8970        ctx.close().await;
8971        mock_ctx.close().await;
8972    }
8973
8974    #[tokio::test(flavor = "multi_thread")]
8975    async fn test_edit_line_when_editing_its_end_point() {
8976        let initial_source = "\
8977sketch(on = XY) {
8978  line(start = [var 1, var 2], end = [var 3, var 4])
8979}
8980";
8981
8982        let program = Program::parse(initial_source).unwrap().0.unwrap();
8983
8984        let mut frontend = FrontendState::new();
8985
8986        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8987        let mock_ctx = ExecutorContext::new_mock(None).await;
8988        let version = Version(0);
8989
8990        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8991        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8992        let sketch_id = sketch_object.id;
8993        let sketch = expect_sketch(sketch_object);
8994        let point_id = *sketch.segments.get(1).unwrap();
8995
8996        let point_ctor = PointCtor {
8997            position: Point2d {
8998                x: Expr::Var(Number {
8999                    value: 5.0,
9000                    units: NumericSuffix::Inch,
9001                }),
9002                y: Expr::Var(Number {
9003                    value: 6.0,
9004                    units: NumericSuffix::Inch,
9005                }),
9006            },
9007        };
9008        let segments = vec![ExistingSegmentCtor {
9009            id: point_id,
9010            ctor: SegmentCtor::Point(point_ctor),
9011        }];
9012        let (src_delta, scene_delta) = frontend
9013            .edit_segments(&mock_ctx, version, sketch_id, segments)
9014            .await
9015            .unwrap();
9016        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9017        assert_eq!(scene_delta.new_objects, vec![]);
9018        assert_eq!(
9019            scene_delta.new_graph.objects.len(),
9020            5,
9021            "{:#?}",
9022            scene_delta.new_graph.objects
9023        );
9024
9025        ctx.close().await;
9026        mock_ctx.close().await;
9027    }
9028
9029    #[tokio::test(flavor = "multi_thread")]
9030    async fn test_edit_line_with_coincident_feedback() {
9031        let initial_source = "\
9032sketch(on = XY) {
9033  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9034  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9035  fixed([line1.start, [0, 0]])
9036  coincident([line1.end, line2.start])
9037  equalLength([line1, line2])
9038}
9039";
9040
9041        let program = Program::parse(initial_source).unwrap().0.unwrap();
9042
9043        let mut frontend = FrontendState::new();
9044
9045        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9046        let mock_ctx = ExecutorContext::new_mock(None).await;
9047        let version = Version(0);
9048
9049        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9050        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9051        let sketch_id = sketch_object.id;
9052        let sketch = expect_sketch(sketch_object);
9053        let line2_end_id = *sketch.segments.get(4).unwrap();
9054
9055        let segments = vec![ExistingSegmentCtor {
9056            id: line2_end_id,
9057            ctor: SegmentCtor::Point(PointCtor {
9058                position: Point2d {
9059                    x: Expr::Var(Number {
9060                        value: 9.0,
9061                        units: NumericSuffix::None,
9062                    }),
9063                    y: Expr::Var(Number {
9064                        value: 10.0,
9065                        units: NumericSuffix::None,
9066                    }),
9067                },
9068            }),
9069        }];
9070        let (src_delta, scene_delta) = frontend
9071            .edit_segments(&mock_ctx, version, sketch_id, segments)
9072            .await
9073            .unwrap();
9074        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9075        assert_eq!(
9076            scene_delta.new_graph.objects.len(),
9077            11,
9078            "{:#?}",
9079            scene_delta.new_graph.objects
9080        );
9081
9082        ctx.close().await;
9083        mock_ctx.close().await;
9084    }
9085
9086    #[tokio::test(flavor = "multi_thread")]
9087    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9088        let initial_source = "\
9089sketch(on = XY) {
9090  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9091  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9092  fixed([line1.start, [0, 0]])
9093  coincident([line1.end, line2.start])
9094  equalLength([line1, line2])
9095}
9096";
9097
9098        let program = Program::parse(initial_source).unwrap().0.unwrap();
9099        let mut frontend = FrontendState::new();
9100        let mock_ctx = ExecutorContext::new_mock(None).await;
9101        let version = Version(0);
9102
9103        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9104        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9105        let sketch_id = sketch_object.id;
9106        let sketch = expect_sketch(sketch_object);
9107        let line2_end_id = *sketch.segments.get(4).unwrap();
9108
9109        let segments = vec![ExistingSegmentCtor {
9110            id: line2_end_id,
9111            ctor: SegmentCtor::Point(PointCtor {
9112                position: Point2d {
9113                    x: Expr::Var(Number {
9114                        value: 9.0,
9115                        units: NumericSuffix::None,
9116                    }),
9117                    y: Expr::Var(Number {
9118                        value: 10.0,
9119                        units: NumericSuffix::None,
9120                    }),
9121                },
9122            }),
9123        }];
9124        let (edited_source, _) = frontend
9125            .edit_segments(&mock_ctx, version, sketch_id, segments)
9126            .await
9127            .unwrap();
9128
9129        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9130        assert_eq!(mock_source.text, edited_source.text);
9131
9132        mock_ctx.close().await;
9133    }
9134
9135    /// Preview segment edits should return solved geometry without persisting
9136    /// solver feedback to KCL.
9137    #[tokio::test(flavor = "multi_thread")]
9138    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9139        let initial_source = "\
9140sketch(on = XY) {
9141  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9142  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9143  fixed([line1.start, [0, 0]])
9144  coincident([line1.end, line2.start])
9145  equalLength([line1, line2])
9146}
9147";
9148
9149        let program = Program::parse(initial_source).unwrap().0.unwrap();
9150        let mut frontend = FrontendState::new();
9151        let mock_ctx = ExecutorContext::new_mock(None).await;
9152        let version = Version(0);
9153
9154        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9155        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9156        let sketch_id = sketch_object.id;
9157        let sketch = expect_sketch(sketch_object);
9158        let line2_end_id = *sketch.segments.get(4).unwrap();
9159
9160        let segments = vec![ExistingSegmentCtor {
9161            id: line2_end_id,
9162            ctor: SegmentCtor::Point(PointCtor {
9163                position: Point2d {
9164                    x: Expr::Var(Number {
9165                        value: 9.0,
9166                        units: NumericSuffix::None,
9167                    }),
9168                    y: Expr::Var(Number {
9169                        value: 10.0,
9170                        units: NumericSuffix::None,
9171                    }),
9172                },
9173            }),
9174        }];
9175        let (preview_source, preview_delta) = frontend
9176            .edit_segments_with_options(
9177                &mock_ctx,
9178                version,
9179                sketch_id,
9180                segments,
9181                EditSegmentsOptions {
9182                    anchor_segment_ids: Some(vec![line2_end_id]),
9183                    drag_anchors: Vec::new(),
9184                    constraint_label_edits: Vec::new(),
9185                    commit_solved_initial_guesses: false,
9186                },
9187            )
9188            .await
9189            .unwrap();
9190
9191        assert!(
9192            !preview_delta.exec_outcome.var_solutions.is_empty(),
9193            "preview solve should still solve and return geometry feedback"
9194        );
9195        assert!(
9196            preview_source
9197                .text
9198                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9199        );
9200        assert!(
9201            preview_source
9202                .text
9203                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9204        );
9205
9206        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9207        assert_eq!(mock_source.text, preview_source.text);
9208
9209        mock_ctx.close().await;
9210    }
9211
9212    #[tokio::test(flavor = "multi_thread")]
9213    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9214        let initial_source = "\
9215sketch(on = XY) {
9216  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9217}
9218";
9219
9220        let program = Program::parse(initial_source).unwrap().0.unwrap();
9221        let mut frontend = FrontendState::new();
9222        let mock_ctx = ExecutorContext::new_mock(None).await;
9223        let version = Version(0);
9224
9225        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9226        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9227        let sketch_id = sketch_object.id;
9228        let sketch = expect_sketch(sketch_object);
9229        let line_end_id = *sketch.segments.get(1).unwrap();
9230
9231        let constraint = Constraint::Fixed(Fixed {
9232            points: vec![FixedPoint {
9233                point: line_end_id,
9234                position: Point2d {
9235                    x: Number {
9236                        value: 20.0,
9237                        units: NumericSuffix::Mm,
9238                    },
9239                    y: Number {
9240                        value: 0.0,
9241                        units: NumericSuffix::Mm,
9242                    },
9243                },
9244            }],
9245        });
9246        let (constraint_source, _) = frontend
9247            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9248            .await
9249            .unwrap();
9250
9251        assert!(
9252            constraint_source
9253                .text
9254                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9255            "{}",
9256            constraint_source.text
9257        );
9258        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9259        assert_eq!(mock_source.text, constraint_source.text);
9260
9261        mock_ctx.close().await;
9262    }
9263
9264    #[test]
9265    fn test_no_solver_feedback_preserves_original_source() {
9266        let initial_source = "\
9267@settings(defaultLengthUnit = in, kclVersion = 2.0)
9268cylinder = startSketchOn(XY)
9269    |> circle(center= [0, 0], radius= 22)
9270    |> extrude(length = 14)
9271";
9272        let mut frontend = FrontendState::new();
9273        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9274        let outcome = ExecOutcome {
9275            variables: Default::default(),
9276            test_program_memory: Default::default(),
9277            operations: Default::default(),
9278            artifact_graph: Default::default(),
9279            scene_objects: Default::default(),
9280            source_range_to_object: Default::default(),
9281            var_solutions: Default::default(),
9282            refactor_metadata: Default::default(),
9283            issues: Default::default(),
9284            filenames: Default::default(),
9285            source_files: Default::default(),
9286            default_planes: Default::default(),
9287        };
9288
9289        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9290
9291        assert_eq!(source_delta.text, initial_source);
9292    }
9293
9294    /// Explicit drag anchors should limit which edited points become temporary
9295    /// fixed constraints.
9296    #[tokio::test(flavor = "multi_thread")]
9297    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9298        let initial_source = "\
9299sketch(on = XY) {
9300  point1 = point(at = [var 0mm, var 0mm])
9301  point2 = point(at = [var 0mm, var 0mm])
9302  coincident([point1, point2])
9303}
9304";
9305
9306        let program = Program::parse(initial_source).unwrap().0.unwrap();
9307        let mut frontend = FrontendState::new();
9308        let mock_ctx = ExecutorContext::new_mock(None).await;
9309        let version = Version(0);
9310
9311        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9312        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9313        let sketch_id = sketch_object.id;
9314        let sketch = expect_sketch(sketch_object);
9315        let point1_id = sketch.segments[0];
9316        let point2_id = sketch.segments[1];
9317
9318        let segments = vec![
9319            ExistingSegmentCtor {
9320                id: point1_id,
9321                ctor: SegmentCtor::Point(PointCtor {
9322                    position: point_expr_mm(10.0, 0.0),
9323                }),
9324            },
9325            ExistingSegmentCtor {
9326                id: point2_id,
9327                ctor: SegmentCtor::Point(PointCtor {
9328                    position: point_expr_mm(100.0, 0.0),
9329                }),
9330            },
9331        ];
9332        let (_, scene_delta) = frontend
9333            .edit_segments_with_options(
9334                &mock_ctx,
9335                version,
9336                sketch_id,
9337                segments,
9338                EditSegmentsOptions {
9339                    anchor_segment_ids: Some(vec![point1_id]),
9340                    drag_anchors: Vec::new(),
9341                    constraint_label_edits: Vec::new(),
9342                    commit_solved_initial_guesses: true,
9343                },
9344            )
9345            .await
9346            .unwrap();
9347
9348        assert_point_position_close(
9349            point_position(&scene_delta.new_graph, point1_id),
9350            point_number_mm(10.0, 0.0),
9351        );
9352        assert_point_position_close(
9353            point_position(&scene_delta.new_graph, point2_id),
9354            point_number_mm(10.0, 0.0),
9355        );
9356
9357        mock_ctx.close().await;
9358    }
9359
9360    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9361    /// for every SketchVar whose initial NumericLiteral has the given value.
9362    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9363        use std::cell::RefCell;
9364        struct Collector {
9365            target: f64,
9366            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9367        }
9368        impl<'a> crate::walk::Visitor<'a> for &Collector {
9369            type Error = crate::front::Error;
9370            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9371                if let crate::walk::Node::SketchVar(sketch_var) = node
9372                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9373                    && (initial.value - self.target).abs() < 1e-9
9374                {
9375                    self.out
9376                        .borrow_mut()
9377                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9378                }
9379                for child in node.children().iter() {
9380                    if !child.visit(*self)? {
9381                        return Ok(false);
9382                    }
9383                }
9384                Ok(true)
9385            }
9386        }
9387        let collector = Collector {
9388            target: value,
9389            out: Default::default(),
9390        };
9391        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9392        collector.out.into_inner()
9393    }
9394
9395    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9396    /// for every SketchVar (including bare `var`).
9397    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9398        use std::cell::RefCell;
9399        struct Collector {
9400            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9401        }
9402        impl<'a> crate::walk::Visitor<'a> for &Collector {
9403            type Error = crate::front::Error;
9404            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9405                if let crate::walk::Node::SketchVar(sketch_var) = node
9406                    && let Some(node_path) = &sketch_var.node_path
9407                {
9408                    self.out
9409                        .borrow_mut()
9410                        .push((SourceRange::from(sketch_var), node_path.clone()));
9411                }
9412                for child in node.children().iter() {
9413                    if !child.visit(*self)? {
9414                        return Ok(false);
9415                    }
9416                }
9417                Ok(true)
9418            }
9419        }
9420        let collector = Collector {
9421            out: Default::default(),
9422        };
9423        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9424        collector.out.into_inner()
9425    }
9426
9427    fn empty_exec_outcome_with_var_solutions(
9428        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9429    ) -> ExecOutcome {
9430        ExecOutcome {
9431            variables: Default::default(),
9432            test_program_memory: Default::default(),
9433            operations: Default::default(),
9434            artifact_graph: Default::default(),
9435            scene_objects: Default::default(),
9436            source_range_to_object: Default::default(),
9437            var_solutions,
9438            refactor_metadata: Default::default(),
9439            issues: Default::default(),
9440            filenames: Default::default(),
9441            source_files: Default::default(),
9442            default_planes: Default::default(),
9443        }
9444    }
9445
9446    /// Happy path: commit a var solution to a `var N` inside a sketch block
9447    /// using a correct NodePath. Confirms the node-path code path produces the
9448    /// expected source mutation.
9449    #[test]
9450    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9451        let initial_source = "\
9452sketch(on = XY) {
9453  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9454}
9455";
9456        let program = Program::parse(initial_source).unwrap().0.unwrap();
9457        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9458        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9459        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9460
9461        let mut frontend = FrontendState::new();
9462        frontend.program = program;
9463
9464        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9465            literal_range,
9466            Some(node_path),
9467            Number {
9468                value: 25.0,
9469                units: NumericSuffix::Mm,
9470            },
9471        )]);
9472
9473        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9474
9475        insta::assert_snapshot!(
9476            "test_commit_var_solution_by_node_path_updates_sketch_var",
9477            source_delta.text
9478        );
9479    }
9480
9481    /// Whitespace inserted earlier in the source shifts the original SketchVar
9482    /// SourceRange. With NodePath propagation the commit should still target
9483    /// the right `var`. We simulate this by collecting node_paths against a
9484    /// "compact" source, then loading the frontend with a "padded" source
9485    /// (whose byte offsets differ), and feeding the original (now stale)
9486    /// source range plus the correct node_path back into the commit.
9487    #[test]
9488    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9489        let compact_source = "\
9490sketch(on = XY) {
9491  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9492}
9493";
9494        let padded_source = "\
9495// added comment\n// added comment\n\nsketch(on = XY) {
9496  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9497}
9498";
9499        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9500        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9501
9502        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9503            .into_iter()
9504            .next()
9505            .expect("expected `var 10mm` in compact source");
9506        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9507            .into_iter()
9508            .next()
9509            .expect("expected `var 10mm` in padded source");
9510
9511        assert_ne!(
9512            compact_match.0, padded_match.0,
9513            "byte offsets must differ for this test to be meaningful"
9514        );
9515        assert_eq!(
9516            compact_match.1, padded_match.1,
9517            "node paths must agree across whitespace; that's the whole point of NodePath",
9518        );
9519
9520        let mut frontend = FrontendState::new();
9521        frontend.program = padded_program;
9522
9523        // Stale source range from the compact source + correct node_path.
9524        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9525            compact_match.0,
9526            Some(compact_match.1),
9527            Number {
9528                value: 30.0,
9529                units: NumericSuffix::Mm,
9530            },
9531        )]);
9532
9533        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9534
9535        insta::assert_snapshot!(
9536            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9537            source_delta.text
9538        );
9539    }
9540
9541    /// When multiple `var` declarations exist and the stale source range
9542    /// happens to land on a *different* var, the node_path must take
9543    /// precedence and the right var gets updated.
9544    #[test]
9545    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9546        let initial_source = "\
9547sketch(on = XY) {
9548  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9549}
9550";
9551        let program = Program::parse(initial_source).unwrap().0.unwrap();
9552
9553        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9554            .into_iter()
9555            .next()
9556            .expect("expected `var 10mm`");
9557        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9558            .into_iter()
9559            .next()
9560            .expect("expected `var 20mm`");
9561
9562        let mut frontend = FrontendState::new();
9563        frontend.program = program;
9564
9565        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9566        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9567            var_20.0,
9568            Some(var_10.1),
9569            Number {
9570                value: 33.0,
9571                units: NumericSuffix::Mm,
9572            },
9573        )]);
9574
9575        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9576
9577        insta::assert_snapshot!(
9578            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9579            source_delta.text
9580        );
9581    }
9582
9583    /// Bare `var` (no initial literal) is only locatable via node_path. With
9584    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9585    /// solver solution should fill the initial value in. The
9586    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9587    /// is gated as an experimental feature; without it the re-parse of the
9588    /// recast source rejects bare `var` declarations.
9589    #[test]
9590    fn test_commit_var_solution_writes_back_into_bare_var() {
9591        let initial_source = "\
9592@settings(experimentalFeatures = allow, kclVersion = 2.0)
9593sketch(on = XY) {
9594  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9595}
9596";
9597        let program = Program::parse(initial_source).unwrap().0.unwrap();
9598
9599        // Pick the first bare `var`; collect_all_sketch_vars returns every
9600        // SketchVar, including bare ones.
9601        let bare = collect_all_sketch_vars(&program)
9602            .into_iter()
9603            .find(|(range, _)| {
9604                // The bare `var` is exactly the 3 characters "var".
9605                range.end() - range.start() == 3
9606            })
9607            .expect("expected at least one bare `var`");
9608
9609        let mut frontend = FrontendState::new();
9610        frontend.program = program;
9611
9612        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9613            bare.0,
9614            Some(bare.1),
9615            Number {
9616                value: 7.0,
9617                units: NumericSuffix::Mm,
9618            },
9619        )]);
9620
9621        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9622
9623        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9624        // written as an explicit suffix so the bare var commits with units.
9625        // The recast adds a blank line after the `@settings` annotation.
9626        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9627    }
9628
9629    #[tokio::test(flavor = "multi_thread")]
9630    async fn test_delete_point_without_var() {
9631        let initial_source = "\
9632sketch(on = XY) {
9633  point(at = [var 1, var 2])
9634  point(at = [var 3, var 4])
9635  point(at = [var 5, var 6])
9636}
9637";
9638
9639        let program = Program::parse(initial_source).unwrap().0.unwrap();
9640
9641        let mut frontend = FrontendState::new();
9642
9643        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9644        let mock_ctx = ExecutorContext::new_mock(None).await;
9645        let version = Version(0);
9646
9647        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9648        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9649        let sketch_id = sketch_object.id;
9650        let sketch = expect_sketch(sketch_object);
9651
9652        let point_id = *sketch.segments.get(1).unwrap();
9653
9654        let (src_delta, scene_delta) = frontend
9655            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9656            .await
9657            .unwrap();
9658        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9659        assert_eq!(scene_delta.new_objects, vec![]);
9660        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9661
9662        ctx.close().await;
9663        mock_ctx.close().await;
9664    }
9665
9666    #[tokio::test(flavor = "multi_thread")]
9667    async fn test_delete_point_with_var() {
9668        let initial_source = "\
9669sketch(on = XY) {
9670  point(at = [var 1, var 2])
9671  point1 = point(at = [var 3, var 4])
9672  point(at = [var 5, var 6])
9673}
9674";
9675
9676        let program = Program::parse(initial_source).unwrap().0.unwrap();
9677
9678        let mut frontend = FrontendState::new();
9679
9680        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9681        let mock_ctx = ExecutorContext::new_mock(None).await;
9682        let version = Version(0);
9683
9684        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9685        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9686        let sketch_id = sketch_object.id;
9687        let sketch = expect_sketch(sketch_object);
9688
9689        let point_id = *sketch.segments.get(1).unwrap();
9690
9691        let (src_delta, scene_delta) = frontend
9692            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9693            .await
9694            .unwrap();
9695        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9696        assert_eq!(scene_delta.new_objects, vec![]);
9697        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9698
9699        ctx.close().await;
9700        mock_ctx.close().await;
9701    }
9702
9703    #[tokio::test(flavor = "multi_thread")]
9704    async fn test_delete_multiple_points() {
9705        let initial_source = "\
9706sketch(on = XY) {
9707  point(at = [var 1, var 2])
9708  point1 = point(at = [var 3, var 4])
9709  point(at = [var 5, var 6])
9710}
9711";
9712
9713        let program = Program::parse(initial_source).unwrap().0.unwrap();
9714
9715        let mut frontend = FrontendState::new();
9716
9717        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9718        let mock_ctx = ExecutorContext::new_mock(None).await;
9719        let version = Version(0);
9720
9721        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9722        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9723        let sketch_id = sketch_object.id;
9724
9725        let sketch = expect_sketch(sketch_object);
9726
9727        let point1_id = *sketch.segments.first().unwrap();
9728        let point2_id = *sketch.segments.get(1).unwrap();
9729
9730        let (src_delta, scene_delta) = frontend
9731            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9732            .await
9733            .unwrap();
9734        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9735        assert_eq!(scene_delta.new_objects, vec![]);
9736        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9737
9738        ctx.close().await;
9739        mock_ctx.close().await;
9740    }
9741
9742    #[tokio::test(flavor = "multi_thread")]
9743    async fn test_delete_coincident_constraint() {
9744        let initial_source = "\
9745sketch(on = XY) {
9746  point1 = point(at = [var 1, var 2])
9747  point2 = point(at = [var 3, var 4])
9748  coincident([point1, point2])
9749  point(at = [var 5, var 6])
9750}
9751";
9752
9753        let program = Program::parse(initial_source).unwrap().0.unwrap();
9754
9755        let mut frontend = FrontendState::new();
9756
9757        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9758        let mock_ctx = ExecutorContext::new_mock(None).await;
9759        let version = Version(0);
9760
9761        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9762        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9763        let sketch_id = sketch_object.id;
9764        let sketch = expect_sketch(sketch_object);
9765
9766        let coincident_id = *sketch.constraints.first().unwrap();
9767
9768        let (src_delta, scene_delta) = frontend
9769            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9770            .await
9771            .unwrap();
9772        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9773        assert_eq!(scene_delta.new_objects, vec![]);
9774        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9775
9776        ctx.close().await;
9777        mock_ctx.close().await;
9778    }
9779
9780    #[tokio::test(flavor = "multi_thread")]
9781    async fn test_delete_line_cascades_to_coincident_constraint() {
9782        let initial_source = "\
9783sketch(on = XY) {
9784  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9785  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9786  coincident([line1.end, line2.start])
9787}
9788";
9789
9790        let program = Program::parse(initial_source).unwrap().0.unwrap();
9791
9792        let mut frontend = FrontendState::new();
9793
9794        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9795        let mock_ctx = ExecutorContext::new_mock(None).await;
9796        let version = Version(0);
9797
9798        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9800        let sketch_id = sketch_object.id;
9801        let sketch = expect_sketch(sketch_object);
9802        let line_id = *sketch.segments.get(5).unwrap();
9803
9804        let (src_delta, scene_delta) = frontend
9805            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9806            .await
9807            .unwrap();
9808        insta::assert_snapshot!(
9809            "test_delete_line_cascades_to_coincident_constraint",
9810            src_delta.text.as_str()
9811        );
9812        assert_eq!(
9813            scene_delta.new_graph.objects.len(),
9814            5,
9815            "{:#?}",
9816            scene_delta.new_graph.objects
9817        );
9818
9819        ctx.close().await;
9820        mock_ctx.close().await;
9821    }
9822
9823    #[tokio::test(flavor = "multi_thread")]
9824    async fn test_delete_line_cascades_to_distance_constraint() {
9825        let initial_source = "\
9826sketch(on = XY) {
9827  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9828  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9829  distance([line1.end, line2.start]) == 10mm
9830}
9831";
9832
9833        let program = Program::parse(initial_source).unwrap().0.unwrap();
9834
9835        let mut frontend = FrontendState::new();
9836
9837        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9838        let mock_ctx = ExecutorContext::new_mock(None).await;
9839        let version = Version(0);
9840
9841        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9842        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9843        let sketch_id = sketch_object.id;
9844        let sketch = expect_sketch(sketch_object);
9845        let line_id = *sketch.segments.get(5).unwrap();
9846
9847        let (src_delta, scene_delta) = frontend
9848            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9849            .await
9850            .unwrap();
9851        insta::assert_snapshot!(
9852            "test_delete_line_cascades_to_distance_constraint",
9853            src_delta.text.as_str()
9854        );
9855        assert_eq!(
9856            scene_delta.new_graph.objects.len(),
9857            5,
9858            "{:#?}",
9859            scene_delta.new_graph.objects
9860        );
9861
9862        ctx.close().await;
9863        mock_ctx.close().await;
9864    }
9865
9866    #[tokio::test(flavor = "multi_thread")]
9867    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9868        let initial_source = "\
9869sketch(on = XY) {
9870  point1 = point(at = [var 1, var 2])
9871  point2 = point(at = [var 3, var 4])
9872  horizontalDistance([point1, point2]) == 10mm
9873}
9874";
9875
9876        let program = Program::parse(initial_source).unwrap().0.unwrap();
9877
9878        let mut frontend = FrontendState::new();
9879
9880        let mock_ctx = ExecutorContext::new_mock(None).await;
9881        let version = Version(0);
9882
9883        frontend.program = program.clone();
9884        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9885        frontend.update_state_after_exec(outcome, true);
9886        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9887        let sketch_id = sketch_object.id;
9888        let sketch = expect_sketch(sketch_object);
9889        let point2_id = *sketch.segments.get(1).unwrap();
9890
9891        let (src_delta, scene_delta) = frontend
9892            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9893            .await
9894            .unwrap();
9895        insta::assert_snapshot!(
9896            "test_delete_point_cascades_to_horizontal_distance_constraint",
9897            src_delta.text.as_str()
9898        );
9899        assert_eq!(
9900            scene_delta.new_graph.objects.len(),
9901            3,
9902            "{:#?}",
9903            scene_delta.new_graph.objects
9904        );
9905
9906        mock_ctx.close().await;
9907    }
9908
9909    #[tokio::test(flavor = "multi_thread")]
9910    async fn test_delete_line_cascades_to_fixed_constraint() {
9911        let initial_source = "\
9912sketch(on = XY) {
9913  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9914  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9915  fixed([line1.start, [0, 0]])
9916}
9917";
9918
9919        let program = Program::parse(initial_source).unwrap().0.unwrap();
9920
9921        let mut frontend = FrontendState::new();
9922
9923        let mock_ctx = ExecutorContext::new_mock(None).await;
9924        let version = Version(0);
9925
9926        frontend.program = program.clone();
9927        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9928        frontend.update_state_after_exec(outcome, true);
9929        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9930        let sketch_id = sketch_object.id;
9931        let sketch = expect_sketch(sketch_object);
9932        let line1_id = *sketch.segments.get(2).unwrap();
9933
9934        let (src_delta, scene_delta) = frontend
9935            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9936            .await
9937            .unwrap();
9938        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9939        assert_eq!(
9940            scene_delta.new_graph.objects.len(),
9941            5,
9942            "{:#?}",
9943            scene_delta.new_graph.objects
9944        );
9945
9946        mock_ctx.close().await;
9947    }
9948
9949    #[tokio::test(flavor = "multi_thread")]
9950    async fn test_delete_line_cascades_to_midpoint_constraint() {
9951        let initial_source = "\
9952sketch(on = XY) {
9953  point1 = point(at = [var 1, var 2])
9954  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9955  midpoint(line1, point = point1)
9956}
9957";
9958
9959        let program = Program::parse(initial_source).unwrap().0.unwrap();
9960
9961        let mut frontend = FrontendState::new();
9962
9963        let mock_ctx = ExecutorContext::new_mock(None).await;
9964        let version = Version(0);
9965
9966        frontend.program = program.clone();
9967        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9968        frontend.update_state_after_exec(outcome, true);
9969        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9970        let sketch_id = sketch_object.id;
9971        let sketch = expect_sketch(sketch_object);
9972        let line1_id = *sketch.segments.get(3).unwrap();
9973
9974        let (src_delta, scene_delta) = frontend
9975            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9976            .await
9977            .unwrap();
9978        insta::assert_snapshot!(
9979            "test_delete_line_cascades_to_midpoint_constraint",
9980            src_delta.text.as_str()
9981        );
9982        assert_eq!(
9983            scene_delta.new_graph.objects.len(),
9984            3,
9985            "{:#?}",
9986            scene_delta.new_graph.objects
9987        );
9988
9989        mock_ctx.close().await;
9990    }
9991
9992    #[tokio::test(flavor = "multi_thread")]
9993    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9994        let initial_source = "\
9995sketch(on = XY) {
9996  point1 = point(at = [var 1, var 2])
9997  point2 = point(at = [var 3, var 4])
9998  point3 = point(at = [var 5, var 6])
9999  coincident([point1, point2, point3])
10000}
10001";
10002
10003        let program = Program::parse(initial_source).unwrap().0.unwrap();
10004
10005        let mut frontend = FrontendState::new();
10006
10007        let mock_ctx = ExecutorContext::new_mock(None).await;
10008        let version = Version(0);
10009
10010        frontend.program = program.clone();
10011        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10012        frontend.update_state_after_exec(outcome, true);
10013        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10014        let sketch_id = sketch_object.id;
10015        let sketch = expect_sketch(sketch_object);
10016        let point3_id = *sketch.segments.get(2).unwrap();
10017
10018        let (src_delta, scene_delta) = frontend
10019            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10020            .await
10021            .unwrap();
10022        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10023        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10024        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10025        assert!(
10026            src_delta.text.contains("coincident([point1, point2])"),
10027            "{}",
10028            src_delta.text
10029        );
10030
10031        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10032        let sketch = expect_sketch(sketch_object);
10033        assert_eq!(sketch.segments.len(), 2);
10034        assert_eq!(sketch.constraints.len(), 1);
10035
10036        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10037        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10038            panic!("Expected constraint object");
10039        };
10040        let Constraint::Coincident(coincident) = constraint else {
10041            panic!("Expected coincident constraint");
10042        };
10043        assert_eq!(
10044            coincident.segments,
10045            sketch
10046                .segments
10047                .iter()
10048                .copied()
10049                .map(Into::into)
10050                .collect::<Vec<ConstraintSegment>>()
10051        );
10052
10053        mock_ctx.close().await;
10054    }
10055
10056    #[tokio::test(flavor = "multi_thread")]
10057    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10058        let initial_source = "\
10059sketch(on = XY) {
10060  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10061  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10062  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10063  equalLength([line1, line2, line3])
10064}
10065";
10066
10067        let program = Program::parse(initial_source).unwrap().0.unwrap();
10068
10069        let mut frontend = FrontendState::new();
10070
10071        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10072        let mock_ctx = ExecutorContext::new_mock(None).await;
10073        let version = Version(0);
10074
10075        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10076        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10077        let sketch_id = sketch_object.id;
10078        let sketch = expect_sketch(sketch_object);
10079        let line3_id = *sketch.segments.get(8).unwrap();
10080
10081        let (src_delta, scene_delta) = frontend
10082            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10083            .await
10084            .unwrap();
10085        insta::assert_snapshot!(
10086            "test_delete_line_preserves_multiline_equal_length_constraint",
10087            src_delta.text.as_str()
10088        );
10089
10090        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10091        let sketch = expect_sketch(sketch_object);
10092        assert_eq!(sketch.constraints.len(), 1);
10093
10094        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10095        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10096            panic!("Expected constraint object");
10097        };
10098        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10099            panic!("Expected lines equal length constraint");
10100        };
10101        assert_eq!(lines_equal_length.lines.len(), 2);
10102
10103        ctx.close().await;
10104        mock_ctx.close().await;
10105    }
10106
10107    #[tokio::test(flavor = "multi_thread")]
10108    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10109        let initial_source = "\
10110sketch(on = XY) {
10111  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10112  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10113  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10114  horizontal([line1.end, line2.start, line3.start])
10115}
10116";
10117
10118        let program = Program::parse(initial_source).unwrap().0.unwrap();
10119
10120        let mut frontend = FrontendState::new();
10121
10122        let mock_ctx = ExecutorContext::new_mock(None).await;
10123        let version = Version(0);
10124
10125        frontend.program = program.clone();
10126        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10127        frontend.update_state_after_exec(outcome, true);
10128        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10129        let sketch_id = sketch_object.id;
10130        let sketch = expect_sketch(sketch_object);
10131        let line1_id = *sketch.segments.get(2).unwrap();
10132
10133        let (src_delta, scene_delta) = frontend
10134            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10135            .await
10136            .unwrap();
10137        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10138        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10139        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10140        assert!(
10141            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10142            "{}",
10143            src_delta.text
10144        );
10145
10146        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10147        let sketch = expect_sketch(sketch_object);
10148        assert_eq!(sketch.constraints.len(), 1);
10149
10150        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10151        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10152            panic!("Expected constraint object");
10153        };
10154        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10155            panic!("Expected horizontal points constraint");
10156        };
10157        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10158        assert_eq!(*points, remaining_points);
10159
10160        mock_ctx.close().await;
10161    }
10162
10163    #[tokio::test(flavor = "multi_thread")]
10164    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10165        let initial_source = "\
10166sketch(on = XY) {
10167  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10168  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10169  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10170  vertical([line1.end, line2.start, line3.start])
10171}
10172";
10173
10174        let program = Program::parse(initial_source).unwrap().0.unwrap();
10175
10176        let mut frontend = FrontendState::new();
10177
10178        let mock_ctx = ExecutorContext::new_mock(None).await;
10179        let version = Version(0);
10180
10181        frontend.program = program.clone();
10182        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10183        frontend.update_state_after_exec(outcome, true);
10184        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10185        let sketch_id = sketch_object.id;
10186        let sketch = expect_sketch(sketch_object);
10187        let line1_id = *sketch.segments.get(2).unwrap();
10188
10189        let (src_delta, scene_delta) = frontend
10190            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10191            .await
10192            .unwrap();
10193        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10194        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10195        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10196        assert!(
10197            src_delta.text.contains("vertical([line2.start, line3.start])"),
10198            "{}",
10199            src_delta.text
10200        );
10201
10202        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10203        let sketch = expect_sketch(sketch_object);
10204        assert_eq!(sketch.constraints.len(), 1);
10205
10206        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10207        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10208            panic!("Expected constraint object");
10209        };
10210        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10211            panic!("Expected vertical points constraint");
10212        };
10213        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10214        assert_eq!(*points, remaining_points);
10215
10216        mock_ctx.close().await;
10217    }
10218
10219    #[tokio::test(flavor = "multi_thread")]
10220    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10221        let initial_source = "\
10222sketch(on = XY) {
10223  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10224  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10225  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10226  coincident([line1.end, line2.start, line3.start])
10227}
10228";
10229
10230        let program = Program::parse(initial_source).unwrap().0.unwrap();
10231
10232        let mut frontend = FrontendState::new();
10233
10234        let mock_ctx = ExecutorContext::new_mock(None).await;
10235        let version = Version(0);
10236
10237        frontend.program = program.clone();
10238        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10239        frontend.update_state_after_exec(outcome, true);
10240        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10241        let sketch_id = sketch_object.id;
10242        let sketch = expect_sketch(sketch_object);
10243        let line1_id = *sketch.segments.get(2).unwrap();
10244
10245        let (src_delta, scene_delta) = frontend
10246            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10247            .await
10248            .unwrap();
10249        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10250        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10251        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10252        assert!(
10253            src_delta.text.contains("coincident([line2.start, line3.start])"),
10254            "{}",
10255            src_delta.text
10256        );
10257
10258        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10259        let sketch = expect_sketch(sketch_object);
10260        assert_eq!(sketch.constraints.len(), 1);
10261
10262        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10263        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10264            panic!("Expected constraint object");
10265        };
10266        let Constraint::Coincident(coincident) = constraint else {
10267            panic!("Expected coincident constraint");
10268        };
10269        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10270        assert_eq!(coincident.segments, remaining_segments);
10271
10272        mock_ctx.close().await;
10273    }
10274
10275    #[tokio::test(flavor = "multi_thread")]
10276    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10277        let initial_source = "\
10278sketch(on = XY) {
10279  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10280  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10281  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10282  equalLength([line1, line2, line3])
10283}
10284";
10285
10286        let program = Program::parse(initial_source).unwrap().0.unwrap();
10287
10288        let mut frontend = FrontendState::new();
10289
10290        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10291        let mock_ctx = ExecutorContext::new_mock(None).await;
10292        let version = Version(0);
10293
10294        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10295        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10296        let sketch_id = sketch_object.id;
10297        let sketch = expect_sketch(sketch_object);
10298        let line2_id = *sketch.segments.get(5).unwrap();
10299        let line3_id = *sketch.segments.get(8).unwrap();
10300
10301        let (src_delta, scene_delta) = frontend
10302            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10303            .await
10304            .unwrap();
10305        insta::assert_snapshot!(
10306            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10307            src_delta.text.as_str()
10308        );
10309
10310        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10311        let sketch = expect_sketch(sketch_object);
10312        assert!(sketch.constraints.is_empty());
10313
10314        ctx.close().await;
10315        mock_ctx.close().await;
10316    }
10317
10318    #[tokio::test(flavor = "multi_thread")]
10319    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10320        let initial_source = "\
10321sketch(on = XY) {
10322  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10323  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10324  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10325  parallel([line1, line2, line3])
10326}
10327";
10328
10329        let program = Program::parse(initial_source).unwrap().0.unwrap();
10330
10331        let mut frontend = FrontendState::new();
10332
10333        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10334        let mock_ctx = ExecutorContext::new_mock(None).await;
10335        let version = Version(0);
10336
10337        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10338        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10339        let sketch_id = sketch_object.id;
10340        let sketch = expect_sketch(sketch_object);
10341        let line3_id = *sketch.segments.get(8).unwrap();
10342
10343        let (src_delta, scene_delta) = frontend
10344            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10345            .await
10346            .unwrap();
10347        insta::assert_snapshot!(
10348            "test_delete_line_preserves_multiline_parallel_constraint",
10349            src_delta.text.as_str()
10350        );
10351
10352        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10353        let sketch = expect_sketch(sketch_object);
10354        assert_eq!(sketch.constraints.len(), 1);
10355
10356        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10357        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10358            panic!("Expected constraint object");
10359        };
10360        let Constraint::Parallel(parallel) = constraint else {
10361            panic!("Expected parallel constraint");
10362        };
10363        assert_eq!(parallel.lines.len(), 2);
10364
10365        ctx.close().await;
10366        mock_ctx.close().await;
10367    }
10368
10369    #[tokio::test(flavor = "multi_thread")]
10370    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10371        let initial_source = "\
10372sketch(on = XY) {
10373  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10374  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10375  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10376  parallel([line1, line2, line3])
10377}
10378";
10379
10380        let program = Program::parse(initial_source).unwrap().0.unwrap();
10381
10382        let mut frontend = FrontendState::new();
10383
10384        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10385        let mock_ctx = ExecutorContext::new_mock(None).await;
10386        let version = Version(0);
10387
10388        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10389        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10390        let sketch_id = sketch_object.id;
10391        let sketch = expect_sketch(sketch_object);
10392        let line2_id = *sketch.segments.get(5).unwrap();
10393        let line3_id = *sketch.segments.get(8).unwrap();
10394
10395        let (src_delta, scene_delta) = frontend
10396            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10397            .await
10398            .unwrap();
10399        insta::assert_snapshot!(
10400            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10401            src_delta.text.as_str()
10402        );
10403
10404        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10405        let sketch = expect_sketch(sketch_object);
10406        assert!(sketch.constraints.is_empty());
10407
10408        ctx.close().await;
10409        mock_ctx.close().await;
10410    }
10411
10412    #[tokio::test(flavor = "multi_thread")]
10413    async fn test_delete_line_line_coincident_constraint() {
10414        let initial_source = "\
10415sketch(on = XY) {
10416  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10417  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10418  coincident([line1, line2])
10419}
10420";
10421
10422        let program = Program::parse(initial_source).unwrap().0.unwrap();
10423
10424        let mut frontend = FrontendState::new();
10425
10426        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10427        let mock_ctx = ExecutorContext::new_mock(None).await;
10428        let version = Version(0);
10429
10430        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10431        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10432        let sketch_id = sketch_object.id;
10433        let sketch = expect_sketch(sketch_object);
10434
10435        let coincident_id = *sketch.constraints.first().unwrap();
10436
10437        let (src_delta, scene_delta) = frontend
10438            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10439            .await
10440            .unwrap();
10441        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10442        assert_eq!(scene_delta.new_objects, vec![]);
10443        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10444
10445        ctx.close().await;
10446        mock_ctx.close().await;
10447    }
10448
10449    #[tokio::test(flavor = "multi_thread")]
10450    async fn test_two_points_coincident() {
10451        let initial_source = "\
10452sketch(on = XY) {
10453  point1 = point(at = [var 1, var 2])
10454  point(at = [3, 4])
10455}
10456";
10457
10458        let program = Program::parse(initial_source).unwrap().0.unwrap();
10459
10460        let mut frontend = FrontendState::new();
10461
10462        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10463        let mock_ctx = ExecutorContext::new_mock(None).await;
10464        let version = Version(0);
10465
10466        frontend.hack_set_program(&ctx, program).await.unwrap();
10467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10468        let sketch_id = sketch_object.id;
10469        let sketch = expect_sketch(sketch_object);
10470        let point0_id = *sketch.segments.first().unwrap();
10471        let point1_id = *sketch.segments.get(1).unwrap();
10472
10473        let constraint = Constraint::Coincident(Coincident {
10474            segments: vec![point0_id.into(), point1_id.into()],
10475        });
10476        let (src_delta, scene_delta) = frontend
10477            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10478            .await
10479            .unwrap();
10480        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10481        assert_eq!(
10482            scene_delta.new_graph.objects.len(),
10483            5,
10484            "{:#?}",
10485            scene_delta.new_graph.objects
10486        );
10487
10488        ctx.close().await;
10489        mock_ctx.close().await;
10490    }
10491
10492    #[tokio::test(flavor = "multi_thread")]
10493    async fn test_three_points_coincident() {
10494        let initial_source = "\
10495sketch(on = XY) {
10496  point1 = point(at = [var 1, var 2])
10497  point(at = [var 3, var 4])
10498  point(at = [var 5, var 6])
10499}
10500";
10501
10502        let program = Program::parse(initial_source).unwrap().0.unwrap();
10503
10504        let mut frontend = FrontendState::new();
10505
10506        let mock_ctx = ExecutorContext::new_mock(None).await;
10507        let version = Version(0);
10508
10509        frontend.program = program.clone();
10510        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10511        frontend.update_state_after_exec(outcome, true);
10512        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10513        let sketch_id = sketch_object.id;
10514        let sketch = expect_sketch(sketch_object);
10515        let segments = sketch
10516            .segments
10517            .iter()
10518            .take(3)
10519            .copied()
10520            .map(Into::into)
10521            .collect::<Vec<ConstraintSegment>>();
10522
10523        let constraint = Constraint::Coincident(Coincident {
10524            segments: segments.clone(),
10525        });
10526        let (src_delta, scene_delta) = frontend
10527            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10528            .await
10529            .unwrap();
10530        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10531
10532        let constraint_object = scene_delta
10533            .new_graph
10534            .objects
10535            .iter()
10536            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10537            .unwrap();
10538
10539        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10540            panic!("expected a constraint object");
10541        };
10542
10543        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10544
10545        mock_ctx.close().await;
10546    }
10547
10548    #[tokio::test(flavor = "multi_thread")]
10549    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10550        let initial_source = "\
10551sketch(on = XY) {
10552  point1 = point(at = [var 1, var 2])
10553  point2 = point(at = [var 3, var 4])
10554  point3 = point(at = [var 5, var 6])
10555  coincident([point1, point2, point3])
10556}
10557";
10558
10559        let program = Program::parse(initial_source).unwrap().0.unwrap();
10560
10561        let mut frontend = FrontendState::new();
10562
10563        let ctx = ExecutorContext::new_mock(None).await;
10564        frontend.program = program.clone();
10565        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10566        frontend.update_state_after_exec(outcome, true);
10567
10568        let constraint_object = frontend
10569            .scene_graph
10570            .objects
10571            .iter()
10572            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10573            .unwrap();
10574        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10575            panic!("expected a constraint object");
10576        };
10577
10578        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10579        let sketch = expect_sketch(sketch_object);
10580        let expected_segments = sketch
10581            .segments
10582            .iter()
10583            .take(3)
10584            .copied()
10585            .map(Into::into)
10586            .collect::<Vec<ConstraintSegment>>();
10587
10588        assert_eq!(
10589            constraint,
10590            &Constraint::Coincident(Coincident {
10591                segments: expected_segments,
10592            })
10593        );
10594
10595        ctx.close().await;
10596    }
10597
10598    #[tokio::test(flavor = "multi_thread")]
10599    async fn test_point_origin_coincident_preserves_order() {
10600        let initial_source = "\
10601sketch(on = XY) {
10602  point(at = [var 1, var 2])
10603}
10604";
10605
10606        for (origin_first, snapshot_name) in [
10607            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10608            (false, "test_point_origin_coincident_preserves_order_point_first"),
10609        ] {
10610            let program = Program::parse(initial_source).unwrap().0.unwrap();
10611
10612            let mut frontend = FrontendState::new();
10613
10614            let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10615            let mock_ctx = ExecutorContext::new_mock(None).await;
10616            let version = Version(0);
10617
10618            frontend.hack_set_program(&ctx, program).await.unwrap();
10619            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10620            let sketch_id = sketch_object.id;
10621            let sketch = expect_sketch(sketch_object);
10622            let point_id = *sketch.segments.first().unwrap();
10623
10624            let segments = if origin_first {
10625                vec![ConstraintSegment::ORIGIN, point_id.into()]
10626            } else {
10627                vec![point_id.into(), ConstraintSegment::ORIGIN]
10628            };
10629            let constraint = Constraint::Coincident(Coincident {
10630                segments: segments.clone(),
10631            });
10632            let (src_delta, scene_delta) = frontend
10633                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10634                .await
10635                .unwrap();
10636            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10637
10638            let constraint_object = scene_delta
10639                .new_graph
10640                .objects
10641                .iter()
10642                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10643                .unwrap();
10644
10645            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10646                panic!("expected a constraint object");
10647            };
10648
10649            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10650
10651            ctx.close().await;
10652            mock_ctx.close().await;
10653        }
10654    }
10655
10656    #[tokio::test(flavor = "multi_thread")]
10657    async fn test_coincident_of_line_end_points() {
10658        let initial_source = "\
10659sketch(on = XY) {
10660  line(start = [var 1, var 2], end = [var 3, var 4])
10661  line(start = [var 5, var 6], end = [var 7, var 8])
10662}
10663";
10664
10665        let program = Program::parse(initial_source).unwrap().0.unwrap();
10666
10667        let mut frontend = FrontendState::new();
10668
10669        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10670        let mock_ctx = ExecutorContext::new_mock(None).await;
10671        let version = Version(0);
10672
10673        frontend.hack_set_program(&ctx, program).await.unwrap();
10674        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10675        let sketch_id = sketch_object.id;
10676        let sketch = expect_sketch(sketch_object);
10677        let point0_id = *sketch.segments.get(1).unwrap();
10678        let point1_id = *sketch.segments.get(3).unwrap();
10679
10680        let constraint = Constraint::Coincident(Coincident {
10681            segments: vec![point0_id.into(), point1_id.into()],
10682        });
10683        let (src_delta, scene_delta) = frontend
10684            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10685            .await
10686            .unwrap();
10687        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10688        assert_eq!(
10689            scene_delta.new_graph.objects.len(),
10690            9,
10691            "{:#?}",
10692            scene_delta.new_graph.objects
10693        );
10694
10695        ctx.close().await;
10696        mock_ctx.close().await;
10697    }
10698
10699    #[tokio::test(flavor = "multi_thread")]
10700    async fn test_coincident_of_line_point_and_circle_segment() {
10701        let initial_source = "\
10702sketch(on = XY) {
10703  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10704  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10705}
10706";
10707        let program = Program::parse(initial_source).unwrap().0.unwrap();
10708        let mut frontend = FrontendState::new();
10709
10710        let mock_ctx = ExecutorContext::new_mock(None).await;
10711        let version = Version(0);
10712
10713        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10714        frontend.program = program;
10715        frontend.update_state_after_exec(outcome, true);
10716        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10717        let sketch_id = sketch_object.id;
10718        let sketch = expect_sketch(sketch_object);
10719
10720        let circle_id = sketch
10721            .segments
10722            .iter()
10723            .copied()
10724            .find(|seg_id| {
10725                matches!(
10726                    &frontend.scene_graph.objects[seg_id.0].kind,
10727                    ObjectKind::Segment {
10728                        segment: Segment::Circle(_)
10729                    }
10730                )
10731            })
10732            .expect("Expected a circle segment in sketch");
10733        let line_id = frontend
10734            .scene_graph
10735            .objects
10736            .iter()
10737            .find_map(|obj| match &obj.kind {
10738                ObjectKind::Segment {
10739                    segment: Segment::Line(line),
10740                } if line.owner.is_none() => Some(obj.id),
10741                _ => None,
10742            })
10743            .expect("Expected a standalone line segment in scene graph");
10744
10745        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10746            ObjectKind::Segment {
10747                segment: Segment::Line(line),
10748            } => line.start,
10749            _ => panic!("Expected line segment object"),
10750        };
10751
10752        let constraint = Constraint::Coincident(Coincident {
10753            segments: vec![line_start_point_id.into(), circle_id.into()],
10754        });
10755        let (src_delta, _scene_delta) = frontend
10756            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10757            .await
10758            .unwrap();
10759        insta::assert_snapshot!(
10760            "test_coincident_of_line_point_and_circle_segment",
10761            src_delta.text.as_str()
10762        );
10763
10764        mock_ctx.close().await;
10765    }
10766
10767    #[tokio::test(flavor = "multi_thread")]
10768    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10769        // Test that attempting an invalid coincident constraint (arc and line)
10770        // doesn't corrupt the state, allowing subsequent operations to work.
10771        // This test verifies the transactional fix in add_constraint that prevents
10772        // state corruption when invalid constraints are attempted.
10773        // Example: coincident constraint between an arc segment and a straight line segment
10774        // is geometrically invalid and should fail, but state should remain intact.
10775        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10776        let program = Program::empty();
10777
10778        let mut frontend = FrontendState::new();
10779        frontend.program = program;
10780
10781        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10782        let mock_ctx = ExecutorContext::new_mock(None).await;
10783        let version = Version(0);
10784
10785        let sketch_args = SketchCtor {
10786            on: Plane::Default(PlaneName::Xy),
10787        };
10788        let (_src_delta, _scene_delta, sketch_id) = frontend
10789            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10790            .await
10791            .unwrap();
10792
10793        // Add an arc segment
10794        let arc_ctor = ArcCtor {
10795            start: Point2d {
10796                x: Expr::Var(Number {
10797                    value: 0.0,
10798                    units: NumericSuffix::Mm,
10799                }),
10800                y: Expr::Var(Number {
10801                    value: 0.0,
10802                    units: NumericSuffix::Mm,
10803                }),
10804            },
10805            end: Point2d {
10806                x: Expr::Var(Number {
10807                    value: 10.0,
10808                    units: NumericSuffix::Mm,
10809                }),
10810                y: Expr::Var(Number {
10811                    value: 10.0,
10812                    units: NumericSuffix::Mm,
10813                }),
10814            },
10815            center: Point2d {
10816                x: Expr::Var(Number {
10817                    value: 10.0,
10818                    units: NumericSuffix::Mm,
10819                }),
10820                y: Expr::Var(Number {
10821                    value: 0.0,
10822                    units: NumericSuffix::Mm,
10823                }),
10824            },
10825            direction: None,
10826            construction: None,
10827        };
10828        let (_src_delta, scene_delta) = frontend
10829            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10830            .await
10831            .unwrap();
10832        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10833        let arc_id = *scene_delta.new_objects.last().unwrap();
10834
10835        // Add a line segment
10836        let line_ctor = LineCtor {
10837            start: Point2d {
10838                x: Expr::Var(Number {
10839                    value: 20.0,
10840                    units: NumericSuffix::Mm,
10841                }),
10842                y: Expr::Var(Number {
10843                    value: 0.0,
10844                    units: NumericSuffix::Mm,
10845                }),
10846            },
10847            end: Point2d {
10848                x: Expr::Var(Number {
10849                    value: 30.0,
10850                    units: NumericSuffix::Mm,
10851                }),
10852                y: Expr::Var(Number {
10853                    value: 10.0,
10854                    units: NumericSuffix::Mm,
10855                }),
10856            },
10857            construction: None,
10858        };
10859        let (_src_delta, scene_delta) = frontend
10860            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10861            .await
10862            .unwrap();
10863        // The line is the last object in new_objects (after the 2 points: start, end)
10864        let line_id = *scene_delta.new_objects.last().unwrap();
10865
10866        // Attempt to add an invalid coincident constraint between arc and line
10867        // This should fail during execution, but state should remain intact
10868        let constraint = Constraint::Coincident(Coincident {
10869            segments: vec![arc_id.into(), line_id.into()],
10870        });
10871        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10872
10873        // The constraint addition should fail (invalid constraint)
10874        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10875
10876        // Verify state is not corrupted by checking that we can still access the scene graph
10877        // and that the original segments are still present with their source ranges
10878        let sketch_object_after =
10879            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10880        let sketch_after = expect_sketch(sketch_object_after);
10881
10882        // Verify both segments are still in the sketch
10883        assert!(
10884            sketch_after.segments.contains(&arc_id),
10885            "Arc segment should still exist after failed constraint"
10886        );
10887        assert!(
10888            sketch_after.segments.contains(&line_id),
10889            "Line segment should still exist after failed constraint"
10890        );
10891
10892        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10893        let arc_obj = frontend
10894            .scene_graph
10895            .objects
10896            .get(arc_id.0)
10897            .expect("Arc object should still be accessible");
10898        let line_obj = frontend
10899            .scene_graph
10900            .objects
10901            .get(line_id.0)
10902            .expect("Line object should still be accessible");
10903
10904        // Verify source ranges are still valid (not corrupted)
10905        // Just verify that the objects are still accessible and have the expected types
10906        match &arc_obj.kind {
10907            ObjectKind::Segment {
10908                segment: Segment::Arc(_),
10909            } => {}
10910            _ => panic!("Arc object should still be an arc segment"),
10911        }
10912        match &line_obj.kind {
10913            ObjectKind::Segment {
10914                segment: Segment::Line(_),
10915            } => {}
10916            _ => panic!("Line object should still be a line segment"),
10917        }
10918
10919        ctx.close().await;
10920        mock_ctx.close().await;
10921    }
10922
10923    #[tokio::test(flavor = "multi_thread")]
10924    async fn test_distance_two_points() {
10925        let initial_source = "\
10926sketch(on = XY) {
10927  point(at = [var 1, var 2])
10928  point(at = [var 3, var 4])
10929}
10930";
10931
10932        let program = Program::parse(initial_source).unwrap().0.unwrap();
10933
10934        let mut frontend = FrontendState::new();
10935
10936        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10937        let mock_ctx = ExecutorContext::new_mock(None).await;
10938        let version = Version(0);
10939
10940        frontend.hack_set_program(&ctx, program).await.unwrap();
10941        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10942        let sketch_id = sketch_object.id;
10943        let sketch = expect_sketch(sketch_object);
10944        let point0_id = *sketch.segments.first().unwrap();
10945        let point1_id = *sketch.segments.get(1).unwrap();
10946
10947        let constraint = Constraint::Distance(Distance {
10948            segments: vec![point0_id.into(), point1_id.into()],
10949            distance: Number {
10950                value: 2.0,
10951                units: NumericSuffix::Mm,
10952            },
10953            label_position: None,
10954            source: Default::default(),
10955        });
10956        let (src_delta, scene_delta) = frontend
10957            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10958            .await
10959            .unwrap();
10960        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10961        assert_eq!(
10962            scene_delta.new_graph.objects.len(),
10963            5,
10964            "{:#?}",
10965            scene_delta.new_graph.objects
10966        );
10967
10968        ctx.close().await;
10969        mock_ctx.close().await;
10970    }
10971
10972    #[tokio::test(flavor = "multi_thread")]
10973    async fn test_distance_two_points_with_label() {
10974        let initial_source = "\
10975sketch(on = XY) {
10976  point(at = [var 1, var 2])
10977  point(at = [var 3, var 4])
10978}
10979";
10980
10981        let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983        let mut frontend = FrontendState::new();
10984
10985        let mock_ctx = ExecutorContext::new_mock(None).await;
10986        let version = Version(0);
10987
10988        frontend.program = program.clone();
10989        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10990        frontend.update_state_after_exec(outcome, true);
10991        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10992        let sketch_id = sketch_object.id;
10993        let sketch = expect_sketch(sketch_object);
10994        let point0_id = *sketch.segments.first().unwrap();
10995        let point1_id = *sketch.segments.get(1).unwrap();
10996
10997        let label_position = Point2d {
10998            x: Number {
10999                value: 10.0,
11000                units: NumericSuffix::Mm,
11001            },
11002            y: Number {
11003                value: 11.0,
11004                units: NumericSuffix::Mm,
11005            },
11006        };
11007        let constraint = Constraint::Distance(Distance {
11008            segments: vec![point0_id.into(), point1_id.into()],
11009            distance: Number {
11010                value: 2.0,
11011                units: NumericSuffix::Mm,
11012            },
11013            label_position: Some(label_position.clone()),
11014            source: Default::default(),
11015        });
11016        let (src_delta, scene_delta) = frontend
11017            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11018            .await
11019            .unwrap();
11020        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11021
11022        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11023        let sketch = expect_sketch(sketch_object);
11024        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11025        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11026            panic!("Expected constraint object");
11027        };
11028        let Constraint::Distance(distance) = constraint else {
11029            panic!("Expected distance constraint");
11030        };
11031        assert_eq!(distance.label_position, Some(label_position));
11032
11033        mock_ctx.close().await;
11034    }
11035
11036    #[tokio::test(flavor = "multi_thread")]
11037    async fn test_edit_distance_constraint_label_position() {
11038        let initial_source = "\
11039sketch(on = XY) {
11040  point(at = [var 1, var 2])
11041  point(at = [var 3, var 2])
11042}
11043";
11044
11045        let program = Program::parse(initial_source).unwrap().0.unwrap();
11046
11047        let mut frontend = FrontendState::new();
11048
11049        let mock_ctx = ExecutorContext::new_mock(None).await;
11050        let version = Version(0);
11051
11052        frontend.program = program.clone();
11053        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11054        frontend.update_state_after_exec(outcome, true);
11055        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11056        let sketch_id = sketch_object.id;
11057        let sketch = expect_sketch(sketch_object);
11058        let point0_id = *sketch.segments.first().unwrap();
11059        let point1_id = *sketch.segments.get(1).unwrap();
11060
11061        let constraint = Constraint::Distance(Distance {
11062            segments: vec![point0_id.into(), point1_id.into()],
11063            distance: Number {
11064                value: 2.0,
11065                units: NumericSuffix::Mm,
11066            },
11067            label_position: None,
11068            source: Default::default(),
11069        });
11070        let (_, scene_delta) = frontend
11071            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11072            .await
11073            .unwrap();
11074        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11075        let sketch = expect_sketch(sketch_object);
11076        let constraint_id = sketch.constraints[0];
11077        let label_position = Point2d {
11078            x: Number {
11079                value: 10.0,
11080                units: NumericSuffix::Mm,
11081            },
11082            y: Number {
11083                value: 11.0,
11084                units: NumericSuffix::Mm,
11085            },
11086        };
11087
11088        let (src_delta, scene_delta) = frontend
11089            .edit_distance_constraint_label_position(
11090                &mock_ctx,
11091                version,
11092                sketch_id,
11093                constraint_id,
11094                label_position.clone(),
11095                vec![],
11096            )
11097            .await
11098            .unwrap();
11099        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11100
11101        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11102        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11103            panic!("Expected constraint object");
11104        };
11105        let Constraint::Distance(distance) = constraint else {
11106            panic!("Expected distance constraint");
11107        };
11108        assert_eq!(distance.label_position, Some(label_position));
11109
11110        mock_ctx.close().await;
11111    }
11112
11113    #[tokio::test(flavor = "multi_thread")]
11114    async fn test_edit_distance_constraint_type_and_value() {
11115        let initial_source = "\
11116sketch(on = XY) {
11117  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11118  distance([line1.start, line1.end]) == 5mm
11119}
11120";
11121
11122        let program = Program::parse(initial_source).unwrap().0.unwrap();
11123        let mut frontend = FrontendState::new();
11124        let mock_ctx = ExecutorContext::new_mock(None).await;
11125        let version = Version(0);
11126
11127        frontend.program = program.clone();
11128        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11129        frontend.update_state_after_exec(outcome, true);
11130        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11131        let sketch_id = sketch_object.id;
11132        let sketch = expect_sketch(sketch_object);
11133        let constraint_id = sketch.constraints[0];
11134        let point0_id = sketch.segments[0];
11135        let point1_id = sketch.segments[1];
11136        let label_position = Point2d {
11137            x: Number {
11138                value: 2.0,
11139                units: NumericSuffix::Mm,
11140            },
11141            y: Number {
11142                value: 5.0,
11143                units: NumericSuffix::Mm,
11144            },
11145        };
11146
11147        let (source_delta, scene_delta) = frontend
11148            .edit_distance_constraint_with_options(
11149                &mock_ctx,
11150                version,
11151                sketch_id,
11152                constraint_id,
11153                Constraint::HorizontalDistance(Distance {
11154                    segments: vec![point0_id.into(), point1_id.into()],
11155                    distance: Number {
11156                        value: 4.0,
11157                        units: NumericSuffix::Mm,
11158                    },
11159                    label_position: Some(label_position.clone()),
11160                    source: Default::default(),
11161                }),
11162                EditConstraintOptions {
11163                    commit_solved_initial_guesses: false,
11164                },
11165            )
11166            .await
11167            .unwrap();
11168        assert_eq!(
11169            source_delta.text,
11170            "\
11171sketch(on = XY) {
11172  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11173  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11174}
11175"
11176        );
11177
11178        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11179        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11180            panic!("Expected constraint object");
11181        };
11182        let Constraint::HorizontalDistance(distance) = constraint else {
11183            panic!("Expected horizontal distance constraint");
11184        };
11185        assert_eq!(distance.distance.value, 4.0);
11186        assert_eq!(distance.label_position, Some(label_position));
11187
11188        mock_ctx.close().await;
11189    }
11190
11191    #[tokio::test(flavor = "multi_thread")]
11192    async fn test_edit_angle_constraint_label_position() {
11193        let initial_source = "\
11194sketch(on = XY) {
11195  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11196  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11197  angle([line1, line2]) == 60deg
11198}
11199";
11200
11201        let program = Program::parse(initial_source).unwrap().0.unwrap();
11202        let mut frontend = FrontendState::new();
11203        let mock_ctx = ExecutorContext::new_mock(None).await;
11204        let version = Version(0);
11205
11206        frontend.program = program.clone();
11207        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11208        frontend.update_state_after_exec(outcome, true);
11209        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11210        let sketch_id = sketch_object.id;
11211        let sketch = expect_sketch(sketch_object);
11212        let constraint_id = sketch.constraints[0];
11213        let label_position = Point2d {
11214            x: Number {
11215                value: 10.0,
11216                units: NumericSuffix::Mm,
11217            },
11218            y: Number {
11219                value: 11.0,
11220                units: NumericSuffix::Mm,
11221            },
11222        };
11223
11224        let (src_delta, scene_delta) = frontend
11225            .edit_distance_constraint_label_position(
11226                &mock_ctx,
11227                version,
11228                sketch_id,
11229                constraint_id,
11230                label_position.clone(),
11231                vec![],
11232            )
11233            .await
11234            .unwrap();
11235        assert_eq!(
11236            src_delta.text.as_str(),
11237            "\
11238sketch(on = XY) {
11239  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11240  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11241  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11242}
11243"
11244        );
11245
11246        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11247        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11248            panic!("Expected constraint object");
11249        };
11250        let Constraint::Angle(angle) = constraint else {
11251            panic!("Expected angle constraint");
11252        };
11253        assert_eq!(angle.label_position, Some(label_position));
11254
11255        mock_ctx.close().await;
11256    }
11257
11258    #[tokio::test(flavor = "multi_thread")]
11259    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11260        let initial_source = "\
11261sketch(on = XY) {
11262  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11263  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11264  60deg == angleDimension(lines = [line1, line2], sector = 1)
11265}
11266";
11267
11268        let program = Program::parse(initial_source).unwrap().0.unwrap();
11269        let mut frontend = FrontendState::new();
11270        let mock_ctx = ExecutorContext::new_mock(None).await;
11271        let version = Version(0);
11272
11273        frontend.program = program.clone();
11274        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11275        frontend.update_state_after_exec(outcome, true);
11276        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11277        let sketch_id = sketch_object.id;
11278        let sketch = expect_sketch(sketch_object);
11279        let constraint_id = sketch.constraints[0];
11280        let label_position = Point2d {
11281            x: Number {
11282                value: 10.0,
11283                units: NumericSuffix::Mm,
11284            },
11285            y: Number {
11286                value: 11.0,
11287                units: NumericSuffix::Mm,
11288            },
11289        };
11290
11291        let (src_delta, scene_delta) = frontend
11292            .edit_distance_constraint_label_position(
11293                &mock_ctx,
11294                version,
11295                sketch_id,
11296                constraint_id,
11297                label_position.clone(),
11298                vec![],
11299            )
11300            .await
11301            .unwrap();
11302        assert_eq!(
11303            src_delta.text.as_str(),
11304            "\
11305sketch(on = XY) {
11306  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11307  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11308  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11309}
11310"
11311        );
11312
11313        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11314        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11315            panic!("Expected constraint object");
11316        };
11317        let Constraint::Angle(angle) = constraint else {
11318            panic!("Expected angle constraint");
11319        };
11320        assert_eq!(angle.label_position, Some(label_position));
11321
11322        mock_ctx.close().await;
11323    }
11324
11325    #[tokio::test(flavor = "multi_thread")]
11326    async fn test_edit_angle_constraint() {
11327        let initial_source = "\
11328sketch(on = XY) {
11329  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11330  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11331  angle([line1, line2]) == 60deg
11332}
11333";
11334
11335        let program = Program::parse(initial_source).unwrap().0.unwrap();
11336        let mut frontend = FrontendState::new();
11337        let mock_ctx = ExecutorContext::new_mock(None).await;
11338        let version = Version(0);
11339
11340        frontend.program = program.clone();
11341        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11342        frontend.update_state_after_exec(outcome, true);
11343        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11344        let sketch_id = sketch_object.id;
11345        let sketch = expect_sketch(sketch_object);
11346        let constraint_id = sketch.constraints[0];
11347        let line1_id = *sketch.segments.get(2).unwrap();
11348        let line2_id = *sketch.segments.get(5).unwrap();
11349        let label_position = Point2d {
11350            x: Number {
11351                value: 10.0,
11352                units: NumericSuffix::Mm,
11353            },
11354            y: Number {
11355                value: 11.0,
11356                units: NumericSuffix::Mm,
11357            },
11358        };
11359
11360        let (src_delta, scene_delta) = frontend
11361            .edit_angle_constraint_with_options(
11362                &mock_ctx,
11363                version,
11364                sketch_id,
11365                constraint_id,
11366                Angle {
11367                    lines: vec![line2_id, line1_id],
11368                    angle: Number {
11369                        value: 60.0,
11370                        units: NumericSuffix::Deg,
11371                    },
11372                    sector: Some(3),
11373                    inverse: Some(false),
11374                    label_position: Some(label_position.clone()),
11375                    source: Default::default(),
11376                },
11377                EditConstraintOptions {
11378                    commit_solved_initial_guesses: false,
11379                },
11380            )
11381            .await
11382            .unwrap();
11383        assert_eq!(
11384            src_delta.text.as_str(),
11385            "\
11386sketch(on = XY) {
11387  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11388  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11389  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11390}
11391"
11392        );
11393
11394        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11395        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11396            panic!("Expected constraint object");
11397        };
11398        let Constraint::Angle(angle) = constraint else {
11399            panic!("Expected angle constraint");
11400        };
11401        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11402        assert_eq!(angle.sector, Some(3));
11403        assert_eq!(angle.inverse, Some(false));
11404        assert_eq!(angle.label_position, Some(label_position));
11405
11406        mock_ctx.close().await;
11407    }
11408
11409    #[tokio::test(flavor = "multi_thread")]
11410    async fn test_edit_angle_constraint_with_call_on_right() {
11411        let initial_source = "\
11412sketch(on = XY) {
11413  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11414  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11415  60deg == angle([line1, line2])
11416}
11417";
11418
11419        let program = Program::parse(initial_source).unwrap().0.unwrap();
11420        let mut frontend = FrontendState::new();
11421        let mock_ctx = ExecutorContext::new_mock(None).await;
11422        let version = Version(0);
11423
11424        frontend.program = program.clone();
11425        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11426        frontend.update_state_after_exec(outcome, true);
11427        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11428        let sketch_id = sketch_object.id;
11429        let sketch = expect_sketch(sketch_object);
11430        let constraint_id = sketch.constraints[0];
11431        let line1_id = *sketch.segments.get(2).unwrap();
11432        let line2_id = *sketch.segments.get(5).unwrap();
11433
11434        let (src_delta, _) = frontend
11435            .edit_angle_constraint_with_options(
11436                &mock_ctx,
11437                version,
11438                sketch_id,
11439                constraint_id,
11440                Angle {
11441                    lines: vec![line2_id, line1_id],
11442                    angle: Number {
11443                        value: 60.0,
11444                        units: NumericSuffix::Deg,
11445                    },
11446                    sector: Some(3),
11447                    inverse: Some(false),
11448                    label_position: None,
11449                    source: Default::default(),
11450                },
11451                EditConstraintOptions {
11452                    commit_solved_initial_guesses: false,
11453                },
11454            )
11455            .await
11456            .unwrap();
11457        assert_eq!(
11458            src_delta.text.as_str(),
11459            "\
11460sketch(on = XY) {
11461  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11462  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11463  60deg == angleDimension(lines = [line2, line1], sector = 3)
11464}
11465"
11466        );
11467
11468        mock_ctx.close().await;
11469    }
11470
11471    #[tokio::test(flavor = "multi_thread")]
11472    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11473        let initial_source = "\
11474@settings(kclVersion = 2.0)
11475
11476sketch001 = sketch(on = XZ) {
11477  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11478  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11479  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11480  vertical([line1.start, ORIGIN])
11481}
11482";
11483
11484        let program = Program::parse(initial_source).unwrap().0.unwrap();
11485        let mut frontend = FrontendState::new();
11486        let mock_ctx = ExecutorContext::new_mock(None).await;
11487        let version = Version(0);
11488
11489        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11490        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11491        let sketch_id = sketch_object.id;
11492        let sketch = expect_sketch(sketch_object);
11493        let constraint_id = sketch
11494            .constraints
11495            .iter()
11496            .copied()
11497            .find(|constraint_id| {
11498                matches!(
11499                    frontend.scene_graph.objects[constraint_id.0].kind,
11500                    ObjectKind::Constraint {
11501                        constraint: Constraint::Distance(_)
11502                    }
11503                )
11504            })
11505            .unwrap();
11506        let line1_id = sketch
11507            .segments
11508            .iter()
11509            .copied()
11510            .find(|segment_id| {
11511                matches!(
11512                    frontend.scene_graph.objects[segment_id.0].kind,
11513                    ObjectKind::Segment {
11514                        segment: Segment::Line(_)
11515                    }
11516                )
11517            })
11518            .unwrap();
11519        let label_position = Point2d {
11520            x: Number {
11521                value: 7.0,
11522                units: NumericSuffix::Mm,
11523            },
11524            y: Number {
11525                value: 9.0,
11526                units: NumericSuffix::Mm,
11527            },
11528        };
11529
11530        let (source_delta, scene_delta) = frontend
11531            .edit_segments_with_options(
11532                &mock_ctx,
11533                version,
11534                sketch_id,
11535                vec![ExistingSegmentCtor {
11536                    id: line1_id,
11537                    ctor: SegmentCtor::Line(LineCtor {
11538                        start: point_expr_mm(2.0, 15.55),
11539                        end: point_expr_mm(-4.03, 11.51),
11540                        construction: None,
11541                    }),
11542                }],
11543                EditSegmentsOptions {
11544                    anchor_segment_ids: Some(vec![]),
11545                    drag_anchors: vec![SegmentDragAnchor {
11546                        segment_id: line1_id,
11547                        target: label_position.clone(),
11548                    }],
11549                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11550                        constraint_id,
11551                        label_position: label_position.clone(),
11552                    }],
11553                    commit_solved_initial_guesses: true,
11554                },
11555            )
11556            .await
11557            .unwrap();
11558
11559        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11560        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11561        let ObjectKind::Constraint {
11562            constraint: Constraint::Distance(distance),
11563        } = &constraint_object.kind
11564        else {
11565            panic!("Expected distance constraint object");
11566        };
11567        assert_eq!(distance.label_position, Some(label_position));
11568
11569        let snapped_label_position = Point2d {
11570            x: Number {
11571                value: 8.0,
11572                units: NumericSuffix::Mm,
11573            },
11574            y: Number {
11575                value: 10.0,
11576                units: NumericSuffix::Mm,
11577            },
11578        };
11579        let (source_delta, scene_delta) = frontend
11580            .edit_segments_with_options(
11581                &mock_ctx,
11582                version,
11583                sketch_id,
11584                vec![],
11585                EditSegmentsOptions {
11586                    anchor_segment_ids: Some(vec![line1_id]),
11587                    drag_anchors: vec![],
11588                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11589                        constraint_id,
11590                        label_position: snapped_label_position.clone(),
11591                    }],
11592                    commit_solved_initial_guesses: true,
11593                },
11594            )
11595            .await
11596            .unwrap();
11597
11598        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11599        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11600        let ObjectKind::Constraint {
11601            constraint: Constraint::Distance(distance),
11602        } = &constraint_object.kind
11603        else {
11604            panic!("Expected distance constraint object");
11605        };
11606        assert_eq!(distance.label_position, Some(snapped_label_position));
11607
11608        mock_ctx.close().await;
11609    }
11610
11611    #[tokio::test(flavor = "multi_thread")]
11612    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11613        let initial_source = "\
11614sketch(on = XY) {
11615  point1 = point(at = [var 0mm, var 0mm])
11616  point2 = point(at = [var 10mm, var 0mm])
11617  distance([point1, point2]) == 5mm
11618}
11619";
11620
11621        let program = Program::parse(initial_source).unwrap().0.unwrap();
11622        let mut frontend = FrontendState::new();
11623        let mock_ctx = ExecutorContext::new_mock(None).await;
11624        let version = Version(0);
11625
11626        frontend.program = program.clone();
11627        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11628        frontend.update_state_after_exec(outcome, true);
11629        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11630        let sketch_id = sketch_object.id;
11631        let sketch = expect_sketch(sketch_object);
11632        let point0_id = sketch.segments[0];
11633        let point1_id = sketch.segments[1];
11634        let constraint_id = sketch.constraints[0];
11635
11636        let edited_segments = vec![ExistingSegmentCtor {
11637            id: point0_id,
11638            ctor: SegmentCtor::Point(PointCtor {
11639                position: Point2d {
11640                    x: Expr::Var(Number {
11641                        value: 2.0,
11642                        units: NumericSuffix::Mm,
11643                    }),
11644                    y: Expr::Var(Number {
11645                        value: 1.0,
11646                        units: NumericSuffix::Mm,
11647                    }),
11648                },
11649            }),
11650        }];
11651        let (_, scene_delta) = frontend
11652            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11653            .await
11654            .unwrap();
11655        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11656        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11657
11658        let label_position = Point2d {
11659            x: Number {
11660                value: 3.0,
11661                units: NumericSuffix::Mm,
11662            },
11663            y: Number {
11664                value: 4.0,
11665                units: NumericSuffix::Mm,
11666            },
11667        };
11668        let (_, scene_delta) = frontend
11669            .edit_distance_constraint_label_position(
11670                &mock_ctx,
11671                version,
11672                sketch_id,
11673                constraint_id,
11674                label_position,
11675                vec![point0_id],
11676            )
11677            .await
11678            .unwrap();
11679
11680        assert_point_position_close(
11681            point_position(&scene_delta.new_graph, point0_id),
11682            point0_after_segment_edit,
11683        );
11684        assert_point_position_close(
11685            point_position(&scene_delta.new_graph, point1_id),
11686            point1_after_segment_edit,
11687        );
11688
11689        mock_ctx.close().await;
11690    }
11691
11692    #[tokio::test(flavor = "multi_thread")]
11693    async fn test_distance_point_line() {
11694        let initial_source = "\
11695sketch(on = XY) {
11696  point(at = [var 0, var 5])
11697  line(start = [var 0, var 0], end = [var 10, var 0])
11698}
11699";
11700
11701        let program = Program::parse(initial_source).unwrap().0.unwrap();
11702
11703        let mut frontend = FrontendState::new();
11704
11705        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11706        let mock_ctx = ExecutorContext::new_mock(None).await;
11707        let version = Version(0);
11708
11709        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11710        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11711        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11712        let sketch_id = sketch_object.id;
11713        let sketch = expect_sketch(sketch_object);
11714        let point_id = *sketch.segments.first().unwrap();
11715        let line_id = *sketch
11716            .segments
11717            .iter()
11718            .find(|segment_id| {
11719                matches!(
11720                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11721                    Some(ObjectKind::Segment {
11722                        segment: Segment::Line(_)
11723                    })
11724                )
11725            })
11726            .unwrap();
11727
11728        let label_position = Point2d {
11729            x: Number {
11730                value: 10.0,
11731                units: NumericSuffix::Mm,
11732            },
11733            y: Number {
11734                value: 11.0,
11735                units: NumericSuffix::Mm,
11736            },
11737        };
11738        let constraint = Constraint::Distance(Distance {
11739            segments: vec![point_id.into(), line_id.into()],
11740            distance: Number {
11741                value: 5.0,
11742                units: NumericSuffix::Mm,
11743            },
11744            label_position: Some(label_position.clone()),
11745            source: Default::default(),
11746        });
11747        let (src_delta, scene_delta) = frontend
11748            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11749            .await
11750            .unwrap();
11751        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11752        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11753        let sketch = expect_sketch(sketch_object);
11754        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11755        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11756            panic!("Expected constraint object");
11757        };
11758        let Constraint::Distance(distance) = constraint else {
11759            panic!("Expected distance constraint");
11760        };
11761        assert_eq!(distance.label_position, Some(label_position));
11762
11763        ctx.close().await;
11764        mock_ctx.close().await;
11765    }
11766
11767    #[tokio::test(flavor = "multi_thread")]
11768    async fn test_distance_point_arc() {
11769        let initial_source = "\
11770sketch(on = XY) {
11771  point(at = [var 0, var 8])
11772  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11773}
11774";
11775
11776        let program = Program::parse(initial_source).unwrap().0.unwrap();
11777
11778        let mut frontend = FrontendState::new();
11779
11780        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11781        let mock_ctx = ExecutorContext::new_mock(None).await;
11782        let version = Version(0);
11783
11784        frontend.hack_set_program(&ctx, program).await.unwrap();
11785        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11786        let sketch_id = sketch_object.id;
11787        let sketch = expect_sketch(sketch_object);
11788        let point_id = *sketch.segments.first().unwrap();
11789        let arc_id = *sketch
11790            .segments
11791            .iter()
11792            .find(|segment_id| {
11793                matches!(
11794                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11795                    Some(ObjectKind::Segment {
11796                        segment: Segment::Arc(_)
11797                    })
11798                )
11799            })
11800            .unwrap();
11801
11802        let constraint = Constraint::Distance(Distance {
11803            segments: vec![point_id.into(), arc_id.into()],
11804            distance: Number {
11805                value: 3.0,
11806                units: NumericSuffix::Mm,
11807            },
11808            label_position: None,
11809            source: Default::default(),
11810        });
11811        let (src_delta, _scene_delta) = frontend
11812            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11813            .await
11814            .unwrap();
11815        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11816
11817        ctx.close().await;
11818        mock_ctx.close().await;
11819    }
11820
11821    #[tokio::test(flavor = "multi_thread")]
11822    async fn test_distance_arc_origin() {
11823        let initial_source = "\
11824sketch001 = sketch(on = XY) {
11825  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11826}
11827";
11828
11829        let program = Program::parse(initial_source).unwrap().0.unwrap();
11830
11831        let mut frontend = FrontendState::new();
11832
11833        let mock_ctx = ExecutorContext::new_mock(None).await;
11834        let version = Version(0);
11835
11836        frontend.program = program.clone();
11837        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11838        frontend.update_state_after_exec(outcome, true);
11839        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11840        let sketch_id = sketch_object.id;
11841        let sketch = expect_sketch(sketch_object);
11842        let arc_id = *sketch
11843            .segments
11844            .iter()
11845            .find(|segment_id| {
11846                matches!(
11847                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11848                    Some(ObjectKind::Segment {
11849                        segment: Segment::Arc(_)
11850                    })
11851                )
11852            })
11853            .unwrap();
11854
11855        let constraint = Constraint::Distance(Distance {
11856            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11857            distance: Number {
11858                value: 3.0,
11859                units: NumericSuffix::Mm,
11860            },
11861            label_position: None,
11862            source: Default::default(),
11863        });
11864        let (src_delta, _scene_delta) = frontend
11865            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11866            .await
11867            .unwrap();
11868        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11869
11870        mock_ctx.close().await;
11871    }
11872
11873    #[tokio::test(flavor = "multi_thread")]
11874    async fn test_distance_line_origin() {
11875        let initial_source = "\
11876sketch(on = XY) {
11877  line(start = [var 5, var 0], end = [var 5, var 10])
11878}
11879";
11880
11881        let program = Program::parse(initial_source).unwrap().0.unwrap();
11882
11883        let mut frontend = FrontendState::new();
11884
11885        let mock_ctx = ExecutorContext::new_mock(None).await;
11886        let version = Version(0);
11887
11888        frontend.program = program.clone();
11889        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11890        frontend.update_state_after_exec(outcome, true);
11891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11892        let sketch_id = sketch_object.id;
11893        let sketch = expect_sketch(sketch_object);
11894        let line_id = *sketch
11895            .segments
11896            .iter()
11897            .find(|segment_id| {
11898                matches!(
11899                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11900                    Some(ObjectKind::Segment {
11901                        segment: Segment::Line(_)
11902                    })
11903                )
11904            })
11905            .unwrap();
11906
11907        let constraint = Constraint::Distance(Distance {
11908            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11909            distance: Number {
11910                value: 5.0,
11911                units: NumericSuffix::Mm,
11912            },
11913            label_position: None,
11914            source: Default::default(),
11915        });
11916        let (src_delta, _scene_delta) = frontend
11917            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11918            .await
11919            .unwrap();
11920        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11921
11922        mock_ctx.close().await;
11923    }
11924
11925    #[tokio::test(flavor = "multi_thread")]
11926    async fn test_distance_line_circle() {
11927        let initial_source = "\
11928sketch(on = XY) {
11929  line(start = [var -10, var 8], end = [var 10, var 8])
11930  circle(start = [var 5, var 0], center = [var 0, var 0])
11931}
11932";
11933
11934        let program = Program::parse(initial_source).unwrap().0.unwrap();
11935
11936        let mut frontend = FrontendState::new();
11937
11938        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11939        let mock_ctx = ExecutorContext::new_mock(None).await;
11940        let version = Version(0);
11941
11942        frontend.hack_set_program(&ctx, program).await.unwrap();
11943        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944        let sketch_id = sketch_object.id;
11945        let sketch = expect_sketch(sketch_object);
11946        let line_id = *sketch
11947            .segments
11948            .iter()
11949            .find(|segment_id| {
11950                matches!(
11951                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11952                    Some(ObjectKind::Segment {
11953                        segment: Segment::Line(_)
11954                    })
11955                )
11956            })
11957            .unwrap();
11958        let circle_id = *sketch
11959            .segments
11960            .iter()
11961            .find(|segment_id| {
11962                matches!(
11963                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11964                    Some(ObjectKind::Segment {
11965                        segment: Segment::Circle(_)
11966                    })
11967                )
11968            })
11969            .unwrap();
11970
11971        let constraint = Constraint::Distance(Distance {
11972            segments: vec![line_id.into(), circle_id.into()],
11973            distance: Number {
11974                value: 3.0,
11975                units: NumericSuffix::Mm,
11976            },
11977            label_position: None,
11978            source: Default::default(),
11979        });
11980        let (src_delta, _scene_delta) = frontend
11981            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11982            .await
11983            .unwrap();
11984        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11985
11986        ctx.close().await;
11987        mock_ctx.close().await;
11988    }
11989
11990    #[tokio::test(flavor = "multi_thread")]
11991    async fn test_distance_circle_arc() {
11992        let initial_source = "\
11993sketch(on = XY) {
11994  circle(start = [var 5, var 0], center = [var 0, var 0])
11995  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11996}
11997";
11998
11999        let program = Program::parse(initial_source).unwrap().0.unwrap();
12000
12001        let mut frontend = FrontendState::new();
12002
12003        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12004        let mock_ctx = ExecutorContext::new_mock(None).await;
12005        let version = Version(0);
12006
12007        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12008        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12009        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12010        let sketch_id = sketch_object.id;
12011        let sketch = expect_sketch(sketch_object);
12012        let circle_id = *sketch
12013            .segments
12014            .iter()
12015            .find(|segment_id| {
12016                matches!(
12017                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12018                    Some(ObjectKind::Segment {
12019                        segment: Segment::Circle(_)
12020                    })
12021                )
12022            })
12023            .unwrap();
12024        let arc_id = *sketch
12025            .segments
12026            .iter()
12027            .find(|segment_id| {
12028                matches!(
12029                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12030                    Some(ObjectKind::Segment {
12031                        segment: Segment::Arc(_)
12032                    })
12033                )
12034            })
12035            .unwrap();
12036
12037        let constraint = Constraint::Distance(Distance {
12038            segments: vec![circle_id.into(), arc_id.into()],
12039            distance: Number {
12040                value: 3.0,
12041                units: NumericSuffix::Mm,
12042            },
12043            label_position: None,
12044            source: Default::default(),
12045        });
12046        let (src_delta, _scene_delta) = frontend
12047            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12048            .await
12049            .unwrap();
12050        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12051
12052        ctx.close().await;
12053        mock_ctx.close().await;
12054    }
12055
12056    #[tokio::test(flavor = "multi_thread")]
12057    async fn test_distance_parallel_lines() {
12058        let initial_source = "\
12059sketch(on = XY) {
12060  line(start = [var 0, var 0], end = [var 10, var 0])
12061  line(start = [var 0, var 5], end = [var 10, var 5])
12062}
12063";
12064
12065        let program = Program::parse(initial_source).unwrap().0.unwrap();
12066
12067        let mut frontend = FrontendState::new();
12068
12069        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12070        let mock_ctx = ExecutorContext::new_mock(None).await;
12071        let version = Version(0);
12072
12073        frontend.hack_set_program(&ctx, program).await.unwrap();
12074        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12075        let sketch_id = sketch_object.id;
12076        let sketch = expect_sketch(sketch_object);
12077        let line_ids = sketch
12078            .segments
12079            .iter()
12080            .copied()
12081            .filter(|segment_id| {
12082                matches!(
12083                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12084                    Some(ObjectKind::Segment {
12085                        segment: Segment::Line(_)
12086                    })
12087                )
12088            })
12089            .collect::<Vec<_>>();
12090
12091        let constraint = Constraint::Distance(Distance {
12092            segments: vec![line_ids[0].into(), line_ids[1].into()],
12093            distance: Number {
12094                value: 5.0,
12095                units: NumericSuffix::Mm,
12096            },
12097            label_position: None,
12098            source: Default::default(),
12099        });
12100        let (src_delta, _scene_delta) = frontend
12101            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12102            .await
12103            .unwrap();
12104        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12105
12106        ctx.close().await;
12107        mock_ctx.close().await;
12108    }
12109
12110    #[tokio::test(flavor = "multi_thread")]
12111    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12112        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12113        // one another because the gradient of the residual has no tangential component in this
12114        // configuration. The only path to reducing the residual is shrinking the lengths of the
12115        // lines which are causing the lines to collapse, producing a degenerate output.
12116        let initial_source = "\
12117sketch(on = XY) {
12118  line(start = [var 0, var 0], end = [var 10, var 0])
12119  line(start = [var 0, var 0], end = [var 10, var 10])
12120}
12121";
12122
12123        let program = Program::parse(initial_source).unwrap().0.unwrap();
12124
12125        let mut frontend = FrontendState::new();
12126
12127        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12128        let mock_ctx = ExecutorContext::new_mock(None).await;
12129        let version = Version(0);
12130
12131        frontend.hack_set_program(&ctx, program).await.unwrap();
12132        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12133        let sketch_id = sketch_object.id;
12134        let sketch = expect_sketch(sketch_object);
12135        let line_ids = sketch
12136            .segments
12137            .iter()
12138            .copied()
12139            .filter(|segment_id| {
12140                matches!(
12141                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12142                    Some(ObjectKind::Segment {
12143                        segment: Segment::Line(_)
12144                    })
12145                )
12146            })
12147            .collect::<Vec<_>>();
12148
12149        let constraint = Constraint::Distance(Distance {
12150            segments: vec![line_ids[0].into(), line_ids[1].into()],
12151            distance: Number {
12152                value: 5.0,
12153                units: NumericSuffix::Mm,
12154            },
12155            label_position: None,
12156            source: Default::default(),
12157        });
12158        let (src_delta, _scene_delta) = frontend
12159            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12160            .await
12161            .unwrap();
12162        insta::assert_snapshot!(
12163            "test_distance_non_parallel_lines_lowers_to_distance",
12164            src_delta.text.as_str()
12165        );
12166
12167        ctx.close().await;
12168        mock_ctx.close().await;
12169    }
12170
12171    #[tokio::test(flavor = "multi_thread")]
12172    async fn test_horizontal_distance_two_points() {
12173        let initial_source = "\
12174sketch(on = XY) {
12175  point(at = [var 1, var 2])
12176  point(at = [var 3, var 4])
12177}
12178";
12179
12180        let program = Program::parse(initial_source).unwrap().0.unwrap();
12181
12182        let mut frontend = FrontendState::new();
12183
12184        let mock_ctx = ExecutorContext::new_mock(None).await;
12185        let version = Version(0);
12186
12187        frontend.program = program.clone();
12188        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12189        frontend.update_state_after_exec(outcome, true);
12190        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12191        let sketch_id = sketch_object.id;
12192        let sketch = expect_sketch(sketch_object);
12193        let point0_id = *sketch.segments.first().unwrap();
12194        let point1_id = *sketch.segments.get(1).unwrap();
12195        let label_position = Point2d {
12196            x: Number {
12197                value: 10.0,
12198                units: NumericSuffix::Mm,
12199            },
12200            y: Number {
12201                value: 11.0,
12202                units: NumericSuffix::Mm,
12203            },
12204        };
12205
12206        let constraint = Constraint::HorizontalDistance(Distance {
12207            segments: vec![point0_id.into(), point1_id.into()],
12208            distance: Number {
12209                value: 2.0,
12210                units: NumericSuffix::Mm,
12211            },
12212            label_position: Some(label_position.clone()),
12213            source: Default::default(),
12214        });
12215        let (src_delta, scene_delta) = frontend
12216            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12217            .await
12218            .unwrap();
12219        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12220        assert_eq!(
12221            scene_delta.new_graph.objects.len(),
12222            5,
12223            "{:#?}",
12224            scene_delta.new_graph.objects
12225        );
12226        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12227        let sketch = expect_sketch(sketch_object);
12228        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12229        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12230            panic!("Expected constraint object");
12231        };
12232        let Constraint::HorizontalDistance(distance) = constraint else {
12233            panic!("Expected horizontal distance constraint");
12234        };
12235        assert_eq!(distance.label_position, Some(label_position));
12236
12237        mock_ctx.close().await;
12238    }
12239
12240    #[tokio::test(flavor = "multi_thread")]
12241    async fn test_radius_single_arc_segment() {
12242        let initial_source = "\
12243sketch(on = XY) {
12244  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12245}
12246";
12247
12248        let program = Program::parse(initial_source).unwrap().0.unwrap();
12249
12250        let mut frontend = FrontendState::new();
12251
12252        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12253        let mock_ctx = ExecutorContext::new_mock(None).await;
12254        let version = Version(0);
12255
12256        frontend.hack_set_program(&ctx, program).await.unwrap();
12257        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12258        let sketch_id = sketch_object.id;
12259        let sketch = expect_sketch(sketch_object);
12260        // Find the arc segment (not the points)
12261        let arc_id = sketch
12262            .segments
12263            .iter()
12264            .find(|&seg_id| {
12265                let obj = frontend.scene_graph.objects.get(seg_id.0);
12266                matches!(
12267                    obj.map(|o| &o.kind),
12268                    Some(ObjectKind::Segment {
12269                        segment: Segment::Arc(_)
12270                    })
12271                )
12272            })
12273            .unwrap();
12274
12275        let constraint = Constraint::Radius(Radius {
12276            arc: *arc_id,
12277            radius: Number {
12278                value: 5.0,
12279                units: NumericSuffix::Mm,
12280            },
12281            label_position: None,
12282            source: Default::default(),
12283        });
12284        let (src_delta, scene_delta) = frontend
12285            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12286            .await
12287            .unwrap();
12288        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12289        assert_eq!(
12290            scene_delta.new_graph.objects.len(),
12291            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12292            "{:#?}",
12293            scene_delta.new_graph.objects
12294        );
12295
12296        ctx.close().await;
12297        mock_ctx.close().await;
12298    }
12299
12300    #[tokio::test(flavor = "multi_thread")]
12301    async fn test_radius_single_arc_segment_with_label_position() {
12302        let initial_source = "\
12303sketch(on = XY) {
12304  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12305}
12306";
12307
12308        let program = Program::parse(initial_source).unwrap().0.unwrap();
12309        let mut frontend = FrontendState::new();
12310        let mock_ctx = ExecutorContext::new_mock(None).await;
12311        let version = Version(0);
12312
12313        frontend.program = program.clone();
12314        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12315        frontend.update_state_after_exec(outcome, true);
12316        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12317        let sketch_id = sketch_object.id;
12318        let sketch = expect_sketch(sketch_object);
12319        let arc_id = sketch
12320            .segments
12321            .iter()
12322            .find(|&seg_id| {
12323                let obj = frontend.scene_graph.objects.get(seg_id.0);
12324                matches!(
12325                    obj.map(|o| &o.kind),
12326                    Some(ObjectKind::Segment {
12327                        segment: Segment::Arc(_)
12328                    })
12329                )
12330            })
12331            .unwrap();
12332
12333        let label_position = Point2d {
12334            x: Number {
12335                value: 10.0,
12336                units: NumericSuffix::Mm,
12337            },
12338            y: Number {
12339                value: 11.0,
12340                units: NumericSuffix::Mm,
12341            },
12342        };
12343        let constraint = Constraint::Radius(Radius {
12344            arc: *arc_id,
12345            radius: Number {
12346                value: 5.0,
12347                units: NumericSuffix::Mm,
12348            },
12349            label_position: Some(label_position.clone()),
12350            source: Default::default(),
12351        });
12352        let (src_delta, scene_delta) = frontend
12353            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12354            .await
12355            .unwrap();
12356        insta::assert_snapshot!(
12357            "test_radius_single_arc_segment_with_label_position",
12358            src_delta.text.as_str()
12359        );
12360
12361        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12362        let sketch = expect_sketch(sketch_object);
12363        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12364        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12365            panic!("Expected constraint object");
12366        };
12367        let Constraint::Radius(radius) = constraint else {
12368            panic!("Expected radius constraint");
12369        };
12370        assert_eq!(radius.label_position, Some(label_position));
12371
12372        mock_ctx.close().await;
12373    }
12374
12375    #[tokio::test(flavor = "multi_thread")]
12376    async fn test_edit_radius_constraint_label_position() {
12377        let initial_source = "\
12378sketch(on = XY) {
12379  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12380  radius(arc1) == 5mm
12381}
12382";
12383
12384        let program = Program::parse(initial_source).unwrap().0.unwrap();
12385        let mut frontend = FrontendState::new();
12386        let mock_ctx = ExecutorContext::new_mock(None).await;
12387        let version = Version(0);
12388
12389        frontend.program = program.clone();
12390        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12391        frontend.update_state_after_exec(outcome, true);
12392        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12393        let sketch_id = sketch_object.id;
12394        let sketch = expect_sketch(sketch_object);
12395        let constraint_id = sketch.constraints[0];
12396        let label_position = Point2d {
12397            x: Number {
12398                value: 10.0,
12399                units: NumericSuffix::Mm,
12400            },
12401            y: Number {
12402                value: 11.0,
12403                units: NumericSuffix::Mm,
12404            },
12405        };
12406
12407        let (src_delta, scene_delta) = frontend
12408            .edit_distance_constraint_label_position(
12409                &mock_ctx,
12410                version,
12411                sketch_id,
12412                constraint_id,
12413                label_position.clone(),
12414                vec![],
12415            )
12416            .await
12417            .unwrap();
12418        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12419
12420        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12421        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12422            panic!("Expected constraint object");
12423        };
12424        let Constraint::Radius(radius) = constraint else {
12425            panic!("Expected radius constraint");
12426        };
12427        assert_eq!(radius.label_position, Some(label_position));
12428
12429        mock_ctx.close().await;
12430    }
12431
12432    #[tokio::test(flavor = "multi_thread")]
12433    async fn test_vertical_distance_two_points() {
12434        let initial_source = "\
12435sketch(on = XY) {
12436  point(at = [var 1, var 2])
12437  point(at = [var 3, var 4])
12438}
12439";
12440
12441        let program = Program::parse(initial_source).unwrap().0.unwrap();
12442
12443        let mut frontend = FrontendState::new();
12444
12445        let mock_ctx = ExecutorContext::new_mock(None).await;
12446        let version = Version(0);
12447
12448        frontend.program = program.clone();
12449        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12450        frontend.update_state_after_exec(outcome, true);
12451        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12452        let sketch_id = sketch_object.id;
12453        let sketch = expect_sketch(sketch_object);
12454        let point0_id = *sketch.segments.first().unwrap();
12455        let point1_id = *sketch.segments.get(1).unwrap();
12456        let label_position = Point2d {
12457            x: Number {
12458                value: 10.0,
12459                units: NumericSuffix::Mm,
12460            },
12461            y: Number {
12462                value: 11.0,
12463                units: NumericSuffix::Mm,
12464            },
12465        };
12466
12467        let constraint = Constraint::VerticalDistance(Distance {
12468            segments: vec![point0_id.into(), point1_id.into()],
12469            distance: Number {
12470                value: 2.0,
12471                units: NumericSuffix::Mm,
12472            },
12473            label_position: Some(label_position.clone()),
12474            source: Default::default(),
12475        });
12476        let (src_delta, scene_delta) = frontend
12477            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12478            .await
12479            .unwrap();
12480        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12481        assert_eq!(
12482            scene_delta.new_graph.objects.len(),
12483            5,
12484            "{:#?}",
12485            scene_delta.new_graph.objects
12486        );
12487        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12488        let sketch = expect_sketch(sketch_object);
12489        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12490        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12491            panic!("Expected constraint object");
12492        };
12493        let Constraint::VerticalDistance(distance) = constraint else {
12494            panic!("Expected vertical distance constraint");
12495        };
12496        assert_eq!(distance.label_position, Some(label_position));
12497
12498        mock_ctx.close().await;
12499    }
12500
12501    #[tokio::test(flavor = "multi_thread")]
12502    async fn test_add_fixed_standalone_point() {
12503        let initial_source = "\
12504sketch(on = XY) {
12505  point(at = [var 1, var 2])
12506}
12507";
12508
12509        let program = Program::parse(initial_source).unwrap().0.unwrap();
12510
12511        let mut frontend = FrontendState::new();
12512
12513        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12514        let mock_ctx = ExecutorContext::new_mock(None).await;
12515        let version = Version(0);
12516
12517        frontend.hack_set_program(&ctx, program).await.unwrap();
12518        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12519        let sketch_id = sketch_object.id;
12520        let sketch = expect_sketch(sketch_object);
12521        let point_id = *sketch.segments.first().unwrap();
12522
12523        let (src_delta, scene_delta) = frontend
12524            .add_constraint(
12525                &mock_ctx,
12526                version,
12527                sketch_id,
12528                Constraint::Fixed(Fixed {
12529                    points: vec![FixedPoint {
12530                        point: point_id,
12531                        position: Point2d {
12532                            x: Number {
12533                                value: 2.0,
12534                                units: NumericSuffix::Mm,
12535                            },
12536                            y: Number {
12537                                value: 3.0,
12538                                units: NumericSuffix::Mm,
12539                            },
12540                        },
12541                    }],
12542                }),
12543            )
12544            .await
12545            .unwrap();
12546        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12547        assert_eq!(
12548            scene_delta.new_graph.objects.len(),
12549            4,
12550            "{:#?}",
12551            scene_delta.new_graph.objects
12552        );
12553
12554        ctx.close().await;
12555        mock_ctx.close().await;
12556    }
12557
12558    #[tokio::test(flavor = "multi_thread")]
12559    async fn test_add_fixed_multiple_points() {
12560        let initial_source = "\
12561sketch(on = XY) {
12562  point(at = [var 1, var 2])
12563  point(at = [var 3, var 4])
12564}
12565";
12566
12567        let program = Program::parse(initial_source).unwrap().0.unwrap();
12568
12569        let mut frontend = FrontendState::new();
12570
12571        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12572        let mock_ctx = ExecutorContext::new_mock(None).await;
12573        let version = Version(0);
12574
12575        frontend.hack_set_program(&ctx, program).await.unwrap();
12576        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12577        let sketch_id = sketch_object.id;
12578        let sketch = expect_sketch(sketch_object);
12579        let point0_id = *sketch.segments.first().unwrap();
12580        let point1_id = *sketch.segments.get(1).unwrap();
12581
12582        let (src_delta, scene_delta) = frontend
12583            .add_constraint(
12584                &mock_ctx,
12585                version,
12586                sketch_id,
12587                Constraint::Fixed(Fixed {
12588                    points: vec![
12589                        FixedPoint {
12590                            point: point0_id,
12591                            position: Point2d {
12592                                x: Number {
12593                                    value: 2.0,
12594                                    units: NumericSuffix::Mm,
12595                                },
12596                                y: Number {
12597                                    value: 3.0,
12598                                    units: NumericSuffix::Mm,
12599                                },
12600                            },
12601                        },
12602                        FixedPoint {
12603                            point: point1_id,
12604                            position: Point2d {
12605                                x: Number {
12606                                    value: 4.0,
12607                                    units: NumericSuffix::Mm,
12608                                },
12609                                y: Number {
12610                                    value: 5.0,
12611                                    units: NumericSuffix::Mm,
12612                                },
12613                            },
12614                        },
12615                    ],
12616                }),
12617            )
12618            .await
12619            .unwrap();
12620        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12621        assert_eq!(
12622            scene_delta.new_graph.objects.len(),
12623            6,
12624            "{:#?}",
12625            scene_delta.new_graph.objects
12626        );
12627
12628        ctx.close().await;
12629        mock_ctx.close().await;
12630    }
12631
12632    #[tokio::test(flavor = "multi_thread")]
12633    async fn test_add_fixed_owned_point() {
12634        let initial_source = "\
12635sketch(on = XY) {
12636  line(start = [var 1, var 2], end = [var 3, var 4])
12637}
12638";
12639
12640        let program = Program::parse(initial_source).unwrap().0.unwrap();
12641
12642        let mut frontend = FrontendState::new();
12643
12644        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12645        let mock_ctx = ExecutorContext::new_mock(None).await;
12646        let version = Version(0);
12647
12648        frontend.hack_set_program(&ctx, program).await.unwrap();
12649        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12650        let sketch_id = sketch_object.id;
12651        let sketch = expect_sketch(sketch_object);
12652        let line_start_id = *sketch.segments.first().unwrap();
12653
12654        let (src_delta, scene_delta) = frontend
12655            .add_constraint(
12656                &mock_ctx,
12657                version,
12658                sketch_id,
12659                Constraint::Fixed(Fixed {
12660                    points: vec![FixedPoint {
12661                        point: line_start_id,
12662                        position: Point2d {
12663                            x: Number {
12664                                value: 2.0,
12665                                units: NumericSuffix::Mm,
12666                            },
12667                            y: Number {
12668                                value: 3.0,
12669                                units: NumericSuffix::Mm,
12670                            },
12671                        },
12672                    }],
12673                }),
12674            )
12675            .await
12676            .unwrap();
12677        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12678        assert_eq!(
12679            scene_delta.new_graph.objects.len(),
12680            6,
12681            "{:#?}",
12682            scene_delta.new_graph.objects
12683        );
12684
12685        ctx.close().await;
12686        mock_ctx.close().await;
12687    }
12688
12689    #[tokio::test(flavor = "multi_thread")]
12690    async fn test_radius_error_cases() {
12691        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12692        let mock_ctx = ExecutorContext::new_mock(None).await;
12693        let version = Version(0);
12694
12695        // Test: Single point should error
12696        let initial_source_point = "\
12697sketch(on = XY) {
12698  point(at = [var 1, var 2])
12699}
12700";
12701        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12702        let mut frontend_point = FrontendState::new();
12703        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12704        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12705        let sketch_id_point = sketch_object_point.id;
12706        let sketch_point = expect_sketch(sketch_object_point);
12707        let point_id = *sketch_point.segments.first().unwrap();
12708
12709        let constraint_point = Constraint::Radius(Radius {
12710            arc: point_id,
12711            radius: Number {
12712                value: 5.0,
12713                units: NumericSuffix::Mm,
12714            },
12715            label_position: None,
12716            source: Default::default(),
12717        });
12718        let result_point = frontend_point
12719            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12720            .await;
12721        assert!(result_point.is_err(), "Single point should error for radius");
12722
12723        // Test: Single line segment should error (only arc segments supported)
12724        let initial_source_line = "\
12725sketch(on = XY) {
12726  line(start = [var 1, var 2], end = [var 3, var 4])
12727}
12728";
12729        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12730        let mut frontend_line = FrontendState::new();
12731        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12732        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12733        let sketch_id_line = sketch_object_line.id;
12734        let sketch_line = expect_sketch(sketch_object_line);
12735        let line_id = *sketch_line.segments.first().unwrap();
12736
12737        let constraint_line = Constraint::Radius(Radius {
12738            arc: line_id,
12739            radius: Number {
12740                value: 5.0,
12741                units: NumericSuffix::Mm,
12742            },
12743            label_position: None,
12744            source: Default::default(),
12745        });
12746        let result_line = frontend_line
12747            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12748            .await;
12749        assert!(result_line.is_err(), "Single line segment should error for radius");
12750
12751        ctx.close().await;
12752        mock_ctx.close().await;
12753    }
12754
12755    #[tokio::test(flavor = "multi_thread")]
12756    async fn test_diameter_single_arc_segment() {
12757        let initial_source = "\
12758sketch(on = XY) {
12759  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12760}
12761";
12762
12763        let program = Program::parse(initial_source).unwrap().0.unwrap();
12764
12765        let mut frontend = FrontendState::new();
12766
12767        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12768        let mock_ctx = ExecutorContext::new_mock(None).await;
12769        let version = Version(0);
12770
12771        frontend.hack_set_program(&ctx, program).await.unwrap();
12772        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12773        let sketch_id = sketch_object.id;
12774        let sketch = expect_sketch(sketch_object);
12775        // Find the arc segment (not the points)
12776        let arc_id = sketch
12777            .segments
12778            .iter()
12779            .find(|&seg_id| {
12780                let obj = frontend.scene_graph.objects.get(seg_id.0);
12781                matches!(
12782                    obj.map(|o| &o.kind),
12783                    Some(ObjectKind::Segment {
12784                        segment: Segment::Arc(_)
12785                    })
12786                )
12787            })
12788            .unwrap();
12789
12790        let constraint = Constraint::Diameter(Diameter {
12791            arc: *arc_id,
12792            diameter: Number {
12793                value: 10.0,
12794                units: NumericSuffix::Mm,
12795            },
12796            label_position: None,
12797            source: Default::default(),
12798        });
12799        let (src_delta, scene_delta) = frontend
12800            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12801            .await
12802            .unwrap();
12803        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12804        assert_eq!(
12805            scene_delta.new_graph.objects.len(),
12806            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12807            "{:#?}",
12808            scene_delta.new_graph.objects
12809        );
12810
12811        ctx.close().await;
12812        mock_ctx.close().await;
12813    }
12814
12815    #[tokio::test(flavor = "multi_thread")]
12816    async fn test_diameter_single_arc_segment_with_label_position() {
12817        let initial_source = "\
12818sketch(on = XY) {
12819  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12820}
12821";
12822
12823        let program = Program::parse(initial_source).unwrap().0.unwrap();
12824        let mut frontend = FrontendState::new();
12825        let mock_ctx = ExecutorContext::new_mock(None).await;
12826        let version = Version(0);
12827
12828        frontend.program = program.clone();
12829        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12830        frontend.update_state_after_exec(outcome, true);
12831        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12832        let sketch_id = sketch_object.id;
12833        let sketch = expect_sketch(sketch_object);
12834        let arc_id = sketch
12835            .segments
12836            .iter()
12837            .find(|&seg_id| {
12838                let obj = frontend.scene_graph.objects.get(seg_id.0);
12839                matches!(
12840                    obj.map(|o| &o.kind),
12841                    Some(ObjectKind::Segment {
12842                        segment: Segment::Arc(_)
12843                    })
12844                )
12845            })
12846            .unwrap();
12847
12848        let label_position = Point2d {
12849            x: Number {
12850                value: 10.0,
12851                units: NumericSuffix::Mm,
12852            },
12853            y: Number {
12854                value: 11.0,
12855                units: NumericSuffix::Mm,
12856            },
12857        };
12858        let constraint = Constraint::Diameter(Diameter {
12859            arc: *arc_id,
12860            diameter: Number {
12861                value: 10.0,
12862                units: NumericSuffix::Mm,
12863            },
12864            label_position: Some(label_position.clone()),
12865            source: Default::default(),
12866        });
12867        let (src_delta, scene_delta) = frontend
12868            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12869            .await
12870            .unwrap();
12871        insta::assert_snapshot!(
12872            "test_diameter_single_arc_segment_with_label_position",
12873            src_delta.text.as_str()
12874        );
12875
12876        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12877        let sketch = expect_sketch(sketch_object);
12878        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12879        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12880            panic!("Expected constraint object");
12881        };
12882        let Constraint::Diameter(diameter) = constraint else {
12883            panic!("Expected diameter constraint");
12884        };
12885        assert_eq!(diameter.label_position, Some(label_position));
12886
12887        mock_ctx.close().await;
12888    }
12889
12890    #[tokio::test(flavor = "multi_thread")]
12891    async fn test_edit_diameter_constraint_label_position() {
12892        let initial_source = "\
12893sketch(on = XY) {
12894  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12895  diameter(arc1) == 10mm
12896}
12897";
12898
12899        let program = Program::parse(initial_source).unwrap().0.unwrap();
12900        let mut frontend = FrontendState::new();
12901        let mock_ctx = ExecutorContext::new_mock(None).await;
12902        let version = Version(0);
12903
12904        frontend.program = program.clone();
12905        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12906        frontend.update_state_after_exec(outcome, true);
12907        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12908        let sketch_id = sketch_object.id;
12909        let sketch = expect_sketch(sketch_object);
12910        let constraint_id = sketch.constraints[0];
12911        let label_position = Point2d {
12912            x: Number {
12913                value: 10.0,
12914                units: NumericSuffix::Mm,
12915            },
12916            y: Number {
12917                value: 11.0,
12918                units: NumericSuffix::Mm,
12919            },
12920        };
12921
12922        let (src_delta, scene_delta) = frontend
12923            .edit_distance_constraint_label_position(
12924                &mock_ctx,
12925                version,
12926                sketch_id,
12927                constraint_id,
12928                label_position.clone(),
12929                vec![],
12930            )
12931            .await
12932            .unwrap();
12933        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12934
12935        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12936        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12937            panic!("Expected constraint object");
12938        };
12939        let Constraint::Diameter(diameter) = constraint else {
12940            panic!("Expected diameter constraint");
12941        };
12942        assert_eq!(diameter.label_position, Some(label_position));
12943
12944        mock_ctx.close().await;
12945    }
12946
12947    #[tokio::test(flavor = "multi_thread")]
12948    async fn test_diameter_error_cases() {
12949        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12950        let mock_ctx = ExecutorContext::new_mock(None).await;
12951        let version = Version(0);
12952
12953        // Test: Single point should error
12954        let initial_source_point = "\
12955sketch(on = XY) {
12956  point(at = [var 1, var 2])
12957}
12958";
12959        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12960        let mut frontend_point = FrontendState::new();
12961        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12962        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12963        let sketch_id_point = sketch_object_point.id;
12964        let sketch_point = expect_sketch(sketch_object_point);
12965        let point_id = *sketch_point.segments.first().unwrap();
12966
12967        let constraint_point = Constraint::Diameter(Diameter {
12968            arc: point_id,
12969            diameter: Number {
12970                value: 10.0,
12971                units: NumericSuffix::Mm,
12972            },
12973            label_position: None,
12974            source: Default::default(),
12975        });
12976        let result_point = frontend_point
12977            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12978            .await;
12979        assert!(result_point.is_err(), "Single point should error for diameter");
12980
12981        // Test: Single line segment should error (only arc segments supported)
12982        let initial_source_line = "\
12983sketch(on = XY) {
12984  line(start = [var 1, var 2], end = [var 3, var 4])
12985}
12986";
12987        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12988        let mut frontend_line = FrontendState::new();
12989        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12990        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12991        let sketch_id_line = sketch_object_line.id;
12992        let sketch_line = expect_sketch(sketch_object_line);
12993        let line_id = *sketch_line.segments.first().unwrap();
12994
12995        let constraint_line = Constraint::Diameter(Diameter {
12996            arc: line_id,
12997            diameter: Number {
12998                value: 10.0,
12999                units: NumericSuffix::Mm,
13000            },
13001            label_position: None,
13002            source: Default::default(),
13003        });
13004        let result_line = frontend_line
13005            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13006            .await;
13007        assert!(result_line.is_err(), "Single line segment should error for diameter");
13008
13009        ctx.close().await;
13010        mock_ctx.close().await;
13011    }
13012
13013    #[tokio::test(flavor = "multi_thread")]
13014    async fn test_line_horizontal() {
13015        let initial_source = "\
13016sketch(on = XY) {
13017  line(start = [var 1, var 2], end = [var 3, var 4])
13018}
13019";
13020
13021        let program = Program::parse(initial_source).unwrap().0.unwrap();
13022
13023        let mut frontend = FrontendState::new();
13024
13025        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13026        let mock_ctx = ExecutorContext::new_mock(None).await;
13027        let version = Version(0);
13028
13029        frontend.hack_set_program(&ctx, program).await.unwrap();
13030        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13031        let sketch_id = sketch_object.id;
13032        let sketch = expect_sketch(sketch_object);
13033        let line1_id = *sketch.segments.get(2).unwrap();
13034
13035        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13036        let (src_delta, scene_delta) = frontend
13037            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13038            .await
13039            .unwrap();
13040        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13041        assert_eq!(
13042            scene_delta.new_graph.objects.len(),
13043            6,
13044            "{:#?}",
13045            scene_delta.new_graph.objects
13046        );
13047
13048        ctx.close().await;
13049        mock_ctx.close().await;
13050    }
13051
13052    #[tokio::test(flavor = "multi_thread")]
13053    async fn test_control_point_spline_edge_horizontal() {
13054        let initial_source = "\
13055@settings(experimentalFeatures = allow)
13056splineSketch = sketch(on = XY) {
13057  controlPointSpline1 = controlPointSpline(points = [
13058    [var 0mm, var 0mm],
13059    [var 10mm, var 20mm],
13060    [var 20mm, var 0mm],
13061  ])
13062}
13063";
13064
13065        let program = Program::parse(initial_source).unwrap().0.unwrap();
13066
13067        let mut frontend = FrontendState::new();
13068
13069        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13070        let mock_ctx = ExecutorContext::new_mock(None).await;
13071        let version = Version(0);
13072
13073        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13074        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13075        let sketch_id = sketch_object.id;
13076        let sketch = expect_sketch(sketch_object);
13077        let spline_id = sketch
13078            .segments
13079            .iter()
13080            .copied()
13081            .find(|seg_id| {
13082                matches!(
13083                    &frontend.scene_graph.objects[seg_id.0].kind,
13084                    ObjectKind::Segment {
13085                        segment: Segment::ControlPointSpline(_)
13086                    }
13087                )
13088            })
13089            .expect("Expected a control point spline segment in sketch");
13090        let edge_id = frontend
13091            .scene_graph
13092            .objects
13093            .iter()
13094            .find_map(|obj| match &obj.kind {
13095                ObjectKind::Segment {
13096                    segment: Segment::Line(line),
13097                } if line.owner == Some(spline_id) => Some(obj.id),
13098                _ => None,
13099            })
13100            .expect("Expected an owned control-polygon edge");
13101
13102        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13103        let (src_delta, _) = frontend
13104            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13105            .await
13106            .unwrap();
13107        assert!(
13108            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13109            "Expected horizontal constraint on spline edge, got: {}",
13110            src_delta.text
13111        );
13112
13113        ctx.close().await;
13114        mock_ctx.close().await;
13115    }
13116
13117    #[tokio::test(flavor = "multi_thread")]
13118    async fn test_control_point_spline_edge_angle() {
13119        let initial_source = "\
13120@settings(experimentalFeatures = allow)
13121splineSketch = sketch(on = XY) {
13122  controlPointSpline1 = controlPointSpline(points = [
13123    [var 0mm, var 0mm],
13124    [var 10mm, var 20mm],
13125    [var 20mm, var 0mm],
13126  ])
13127
13128  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13129}
13130";
13131
13132        let program = Program::parse(initial_source).unwrap().0.unwrap();
13133
13134        let mut frontend = FrontendState::new();
13135
13136        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13137        let mock_ctx = ExecutorContext::new_mock(None).await;
13138        let version = Version(0);
13139
13140        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13141        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13142        let sketch_id = sketch_object.id;
13143        let sketch = expect_sketch(sketch_object);
13144        let spline_id = sketch
13145            .segments
13146            .iter()
13147            .copied()
13148            .find(|seg_id| {
13149                matches!(
13150                    &frontend.scene_graph.objects[seg_id.0].kind,
13151                    ObjectKind::Segment {
13152                        segment: Segment::ControlPointSpline(_)
13153                    }
13154                )
13155            })
13156            .expect("Expected a control point spline segment in sketch");
13157        let edge_id = frontend
13158            .scene_graph
13159            .objects
13160            .iter()
13161            .find_map(|obj| match &obj.kind {
13162                ObjectKind::Segment {
13163                    segment: Segment::Line(line),
13164                } if line.owner == Some(spline_id) => Some(obj.id),
13165                _ => None,
13166            })
13167            .expect("Expected an owned control-polygon edge");
13168        let line1_id = frontend
13169            .scene_graph
13170            .objects
13171            .iter()
13172            .find_map(|obj| match &obj.kind {
13173                ObjectKind::Segment {
13174                    segment: Segment::Line(line),
13175                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13176                _ => None,
13177            })
13178            .or_else(|| {
13179                sketch.segments.iter().copied().find(|seg_id| {
13180                    matches!(
13181                        &frontend.scene_graph.objects[seg_id.0].kind,
13182                        ObjectKind::Segment {
13183                            segment: Segment::Line(line),
13184                        } if line.owner.is_none()
13185                    )
13186                })
13187            })
13188            .expect("Expected a standalone line segment in sketch");
13189
13190        let constraint = Constraint::Angle(Angle {
13191            lines: vec![line1_id, edge_id],
13192            angle: Number {
13193                value: 30.0,
13194                units: NumericSuffix::Deg,
13195            },
13196            sector: None,
13197            inverse: None,
13198            label_position: None,
13199            source: Default::default(),
13200        });
13201        let (src_delta, _) = frontend
13202            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13203            .await
13204            .unwrap();
13205        assert!(
13206            src_delta
13207                .text
13208                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13209            "Expected angle constraint on spline edge, got: {}",
13210            src_delta.text
13211        );
13212
13213        ctx.close().await;
13214        mock_ctx.close().await;
13215    }
13216
13217    #[tokio::test(flavor = "multi_thread")]
13218    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13219        let initial_source = "\
13220@settings(experimentalFeatures = allow)
13221splineSketch = sketch(on = XY) {
13222  spline1 = controlPointSpline(points = [
13223    [var 0mm, var 0mm],
13224    [var 10mm, var 20mm],
13225    [var 20mm, var 0mm],
13226  ])
13227  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13228  coincident([spline1.controls[1], spline1.edges[0]])
13229  coincident([spline1.controls[0], line1])
13230}
13231";
13232
13233        let program = Program::parse(initial_source).unwrap().0.unwrap();
13234
13235        let mut frontend = FrontendState::new();
13236
13237        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13238        let mock_ctx = ExecutorContext::new_mock(None).await;
13239
13240        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13241
13242        let ui_scene_graph = frontend.scene_graph_for_ui();
13243        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13244        let sketch = expect_sketch(sketch_object);
13245
13246        assert_eq!(
13247            sketch.constraints.len(),
13248            1,
13249            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13250        );
13251
13252        let visible_constraints = ui_scene_graph
13253            .objects
13254            .iter()
13255            .filter_map(|object| match &object.kind {
13256                ObjectKind::Constraint {
13257                    constraint: Constraint::Coincident(coincident),
13258                } => Some(coincident.clone()),
13259                _ => None,
13260            })
13261            .collect::<Vec<_>>();
13262
13263        assert_eq!(
13264            visible_constraints.len(),
13265            1,
13266            "Expected only one coincident constraint object in the UI scene graph"
13267        );
13268        assert_eq!(
13269            visible_constraints[0].get_segments().len(),
13270            2,
13271            "Expected the remaining visible coincident constraint to reference two segments"
13272        );
13273
13274        ctx.close().await;
13275        mock_ctx.close().await;
13276    }
13277
13278    #[tokio::test(flavor = "multi_thread")]
13279    async fn test_edit_control_point_spline_can_append_control_point() {
13280        let initial_source = "\
13281@settings(experimentalFeatures = allow)
13282splineSketch = sketch(on = XY) {
13283  controlPointSpline(points = [
13284    [var 0mm, var 0mm],
13285    [var 10mm, var 20mm],
13286    [var 20mm, var 0mm],
13287  ])
13288}
13289";
13290
13291        let program = Program::parse(initial_source).unwrap().0.unwrap();
13292
13293        let mut frontend = FrontendState::new();
13294
13295        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13296        let mock_ctx = ExecutorContext::new_mock(None).await;
13297        let version = Version(0);
13298
13299        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13300        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13301        let sketch_id = sketch_object.id;
13302        let sketch = expect_sketch(sketch_object);
13303        let spline_id = sketch
13304            .segments
13305            .iter()
13306            .copied()
13307            .find(|seg_id| {
13308                matches!(
13309                    &frontend.scene_graph.objects[seg_id.0].kind,
13310                    ObjectKind::Segment {
13311                        segment: Segment::ControlPointSpline(_)
13312                    }
13313                )
13314            })
13315            .expect("Expected a control point spline segment in sketch");
13316
13317        let ctor = ControlPointSplineCtor {
13318            points: vec![
13319                Point2d {
13320                    x: Expr::Var(Number {
13321                        value: 0.0,
13322                        units: NumericSuffix::Mm,
13323                    }),
13324                    y: Expr::Var(Number {
13325                        value: 0.0,
13326                        units: NumericSuffix::Mm,
13327                    }),
13328                },
13329                Point2d {
13330                    x: Expr::Var(Number {
13331                        value: 10.0,
13332                        units: NumericSuffix::Mm,
13333                    }),
13334                    y: Expr::Var(Number {
13335                        value: 20.0,
13336                        units: NumericSuffix::Mm,
13337                    }),
13338                },
13339                Point2d {
13340                    x: Expr::Var(Number {
13341                        value: 20.0,
13342                        units: NumericSuffix::Mm,
13343                    }),
13344                    y: Expr::Var(Number {
13345                        value: 0.0,
13346                        units: NumericSuffix::Mm,
13347                    }),
13348                },
13349                Point2d {
13350                    x: Expr::Var(Number {
13351                        value: 30.0,
13352                        units: NumericSuffix::Mm,
13353                    }),
13354                    y: Expr::Var(Number {
13355                        value: 10.0,
13356                        units: NumericSuffix::Mm,
13357                    }),
13358                },
13359            ],
13360            construction: None,
13361        };
13362
13363        let segments = vec![ExistingSegmentCtor {
13364            id: spline_id,
13365            ctor: SegmentCtor::ControlPointSpline(ctor),
13366        }];
13367        let (src_delta, scene_delta) = frontend
13368            .edit_segments(&mock_ctx, version, sketch_id, segments)
13369            .await
13370            .unwrap();
13371
13372        assert!(
13373            src_delta.text.contains("[var 30mm, var 10mm]"),
13374            "Expected appended spline control point in source, got: {}",
13375            src_delta.text
13376        );
13377
13378        assert!(
13379            scene_delta.invalidates_ids,
13380            "Expected appending a spline control point to invalidate ids"
13381        );
13382        let updated_spline = scene_delta
13383            .new_graph
13384            .objects
13385            .iter()
13386            .find_map(|obj| match &obj.kind {
13387                ObjectKind::Segment {
13388                    segment: Segment::ControlPointSpline(updated_spline),
13389                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13390                _ => None,
13391            })
13392            .expect("Expected edited scene graph to contain a four-point control point spline");
13393        assert_eq!(
13394            updated_spline.controls.len(),
13395            4,
13396            "Expected edited spline to expose four control points"
13397        );
13398
13399        ctx.close().await;
13400        mock_ctx.close().await;
13401    }
13402
13403    #[tokio::test(flavor = "multi_thread")]
13404    async fn test_line_vertical() {
13405        let initial_source = "\
13406sketch(on = XY) {
13407  line(start = [var 1, var 2], end = [var 3, var 4])
13408}
13409";
13410
13411        let program = Program::parse(initial_source).unwrap().0.unwrap();
13412
13413        let mut frontend = FrontendState::new();
13414
13415        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13416        let mock_ctx = ExecutorContext::new_mock(None).await;
13417        let version = Version(0);
13418
13419        frontend.hack_set_program(&ctx, program).await.unwrap();
13420        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13421        let sketch_id = sketch_object.id;
13422        let sketch = expect_sketch(sketch_object);
13423        let line1_id = *sketch.segments.get(2).unwrap();
13424
13425        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13426        let (src_delta, scene_delta) = frontend
13427            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13428            .await
13429            .unwrap();
13430        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13431        assert_eq!(
13432            scene_delta.new_graph.objects.len(),
13433            6,
13434            "{:#?}",
13435            scene_delta.new_graph.objects
13436        );
13437
13438        ctx.close().await;
13439        mock_ctx.close().await;
13440    }
13441
13442    #[tokio::test(flavor = "multi_thread")]
13443    async fn test_points_vertical() {
13444        let initial_source = "\
13445sketch001 = sketch(on = XY) {
13446  p0 = point(at = [var -2.23mm, var 3.1mm])
13447  pf = point(at = [4, 4])
13448}
13449";
13450
13451        let program = Program::parse(initial_source).unwrap().0.unwrap();
13452
13453        let mut frontend = FrontendState::new();
13454
13455        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13456        let mock_ctx = ExecutorContext::new_mock(None).await;
13457        let version = Version(0);
13458
13459        frontend.hack_set_program(&ctx, program).await.unwrap();
13460        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13461        let sketch_id = sketch_object.id;
13462        let sketch = expect_sketch(sketch_object);
13463        let point_ids = vec![
13464            sketch.segments.first().unwrap().to_owned(),
13465            sketch.segments.get(1).unwrap().to_owned(),
13466        ];
13467
13468        let constraint = Constraint::Vertical(Vertical::Points {
13469            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13470        });
13471        let (src_delta, scene_delta) = frontend
13472            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13473            .await
13474            .unwrap();
13475        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13476        assert_eq!(
13477            scene_delta.new_graph.objects.len(),
13478            5,
13479            "{:#?}",
13480            scene_delta.new_graph.objects
13481        );
13482
13483        ctx.close().await;
13484        mock_ctx.close().await;
13485    }
13486
13487    #[tokio::test(flavor = "multi_thread")]
13488    async fn test_points_horizontal() {
13489        let initial_source = "\
13490sketch001 = sketch(on = XY) {
13491  p0 = point(at = [var -2.23mm, var 3.1mm])
13492  pf = point(at = [4, 4])
13493}
13494";
13495
13496        let program = Program::parse(initial_source).unwrap().0.unwrap();
13497
13498        let mut frontend = FrontendState::new();
13499
13500        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13501        let mock_ctx = ExecutorContext::new_mock(None).await;
13502        let version = Version(0);
13503
13504        frontend.hack_set_program(&ctx, program).await.unwrap();
13505        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13506        let sketch_id = sketch_object.id;
13507        let sketch = expect_sketch(sketch_object);
13508        let point_ids = vec![
13509            sketch.segments.first().unwrap().to_owned(),
13510            sketch.segments.get(1).unwrap().to_owned(),
13511        ];
13512
13513        let constraint = Constraint::Horizontal(Horizontal::Points {
13514            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13515        });
13516        let (src_delta, scene_delta) = frontend
13517            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13518            .await
13519            .unwrap();
13520        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13521        assert_eq!(
13522            scene_delta.new_graph.objects.len(),
13523            5,
13524            "{:#?}",
13525            scene_delta.new_graph.objects
13526        );
13527
13528        ctx.close().await;
13529        mock_ctx.close().await;
13530    }
13531
13532    #[tokio::test(flavor = "multi_thread")]
13533    async fn test_point_horizontal_with_origin() {
13534        let initial_source = "\
13535sketch001 = sketch(on = XY) {
13536  p0 = point(at = [var -2.23mm, var 3.1mm])
13537}
13538";
13539
13540        let program = Program::parse(initial_source).unwrap().0.unwrap();
13541
13542        let mut frontend = FrontendState::new();
13543
13544        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13545        let mock_ctx = ExecutorContext::new_mock(None).await;
13546        let version = Version(0);
13547
13548        frontend.hack_set_program(&ctx, program).await.unwrap();
13549        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13550        let sketch_id = sketch_object.id;
13551        let sketch = expect_sketch(sketch_object);
13552        let point_id = *sketch.segments.first().unwrap();
13553
13554        let constraint = Constraint::Horizontal(Horizontal::Points {
13555            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13556        });
13557        let (src_delta, scene_delta) = frontend
13558            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13559            .await
13560            .unwrap();
13561        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13562        assert_eq!(
13563            scene_delta.new_graph.objects.len(),
13564            4,
13565            "{:#?}",
13566            scene_delta.new_graph.objects
13567        );
13568
13569        ctx.close().await;
13570        mock_ctx.close().await;
13571    }
13572
13573    #[tokio::test(flavor = "multi_thread")]
13574    async fn test_lines_equal_length() {
13575        let initial_source = "\
13576sketch(on = XY) {
13577  line(start = [var 1, var 2], end = [var 3, var 4])
13578  line(start = [var 5, var 6], end = [var 7, var 8])
13579}
13580";
13581
13582        let program = Program::parse(initial_source).unwrap().0.unwrap();
13583
13584        let mut frontend = FrontendState::new();
13585
13586        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13587        let mock_ctx = ExecutorContext::new_mock(None).await;
13588        let version = Version(0);
13589
13590        frontend.hack_set_program(&ctx, program).await.unwrap();
13591        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13592        let sketch_id = sketch_object.id;
13593        let sketch = expect_sketch(sketch_object);
13594        let line1_id = *sketch.segments.get(2).unwrap();
13595        let line2_id = *sketch.segments.get(5).unwrap();
13596
13597        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13598            lines: vec![line1_id, line2_id],
13599        });
13600        let (src_delta, scene_delta) = frontend
13601            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13602            .await
13603            .unwrap();
13604        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13605        assert_eq!(
13606            scene_delta.new_graph.objects.len(),
13607            9,
13608            "{:#?}",
13609            scene_delta.new_graph.objects
13610        );
13611
13612        ctx.close().await;
13613        mock_ctx.close().await;
13614    }
13615
13616    #[tokio::test(flavor = "multi_thread")]
13617    async fn test_add_constraint_multi_line_equal_length() {
13618        let initial_source = "\
13619sketch(on = XY) {
13620  line(start = [var 1, var 2], end = [var 3, var 4])
13621  line(start = [var 5, var 6], end = [var 7, var 8])
13622  line(start = [var 9, var 10], end = [var 11, var 12])
13623}
13624";
13625
13626        let program = Program::parse(initial_source).unwrap().0.unwrap();
13627
13628        let mut frontend = FrontendState::new();
13629        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13630        let mock_ctx = ExecutorContext::new_mock(None).await;
13631        let version = Version(0);
13632
13633        frontend.hack_set_program(&ctx, program).await.unwrap();
13634        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13635        let sketch_id = sketch_object.id;
13636        let sketch = expect_sketch(sketch_object);
13637        let line1_id = *sketch.segments.get(2).unwrap();
13638        let line2_id = *sketch.segments.get(5).unwrap();
13639        let line3_id = *sketch.segments.get(8).unwrap();
13640
13641        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13642            lines: vec![line1_id, line2_id, line3_id],
13643        });
13644        let (src_delta, scene_delta) = frontend
13645            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13646            .await
13647            .unwrap();
13648        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13649        let constraints = scene_delta
13650            .new_graph
13651            .objects
13652            .iter()
13653            .filter_map(|obj| {
13654                let ObjectKind::Constraint { constraint } = &obj.kind else {
13655                    return None;
13656                };
13657                Some(constraint)
13658            })
13659            .collect::<Vec<_>>();
13660
13661        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13662        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13663            panic!("expected equal length constraint, got {:?}", constraints[0]);
13664        };
13665        assert_eq!(lines_equal_length.lines.len(), 3);
13666
13667        ctx.close().await;
13668        mock_ctx.close().await;
13669    }
13670
13671    #[tokio::test(flavor = "multi_thread")]
13672    async fn test_lines_parallel() {
13673        let initial_source = "\
13674sketch(on = XY) {
13675  line(start = [var 1, var 2], end = [var 3, var 4])
13676  line(start = [var 5, var 6], end = [var 7, var 8])
13677}
13678";
13679
13680        let program = Program::parse(initial_source).unwrap().0.unwrap();
13681
13682        let mut frontend = FrontendState::new();
13683
13684        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13685        let mock_ctx = ExecutorContext::new_mock(None).await;
13686        let version = Version(0);
13687
13688        frontend.hack_set_program(&ctx, program).await.unwrap();
13689        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13690        let sketch_id = sketch_object.id;
13691        let sketch = expect_sketch(sketch_object);
13692        let line1_id = *sketch.segments.get(2).unwrap();
13693        let line2_id = *sketch.segments.get(5).unwrap();
13694
13695        let constraint = Constraint::Parallel(Parallel {
13696            lines: vec![line1_id, line2_id],
13697        });
13698        let (src_delta, scene_delta) = frontend
13699            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13700            .await
13701            .unwrap();
13702        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13703        assert_eq!(
13704            scene_delta.new_graph.objects.len(),
13705            9,
13706            "{:#?}",
13707            scene_delta.new_graph.objects
13708        );
13709
13710        ctx.close().await;
13711        mock_ctx.close().await;
13712    }
13713
13714    #[tokio::test(flavor = "multi_thread")]
13715    async fn test_lines_parallel_multiline() {
13716        let initial_source = "\
13717sketch(on = XY) {
13718  line(start = [var 1, var 2], end = [var 3, var 4])
13719  line(start = [var 5, var 6], end = [var 7, var 8])
13720  line(start = [var 9, var 10], end = [var 11, var 12])
13721}
13722";
13723
13724        let program = Program::parse(initial_source).unwrap().0.unwrap();
13725
13726        let mut frontend = FrontendState::new();
13727
13728        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13729        let mock_ctx = ExecutorContext::new_mock(None).await;
13730        let version = Version(0);
13731
13732        frontend.hack_set_program(&ctx, program).await.unwrap();
13733        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13734        let sketch_id = sketch_object.id;
13735        let sketch = expect_sketch(sketch_object);
13736        let line1_id = *sketch.segments.get(2).unwrap();
13737        let line2_id = *sketch.segments.get(5).unwrap();
13738        let line3_id = *sketch.segments.get(8).unwrap();
13739
13740        let constraint = Constraint::Parallel(Parallel {
13741            lines: vec![line1_id, line2_id, line3_id],
13742        });
13743        let (src_delta, scene_delta) = frontend
13744            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13745            .await
13746            .unwrap();
13747        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13748
13749        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13750        let sketch = expect_sketch(sketch_object);
13751        assert_eq!(sketch.constraints.len(), 1);
13752
13753        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13754        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13755            panic!("Expected constraint object");
13756        };
13757        let Constraint::Parallel(parallel) = constraint else {
13758            panic!("Expected parallel constraint");
13759        };
13760        assert_eq!(parallel.lines.len(), 3);
13761
13762        ctx.close().await;
13763        mock_ctx.close().await;
13764    }
13765
13766    #[tokio::test(flavor = "multi_thread")]
13767    async fn test_lines_perpendicular() {
13768        let initial_source = "\
13769sketch(on = XY) {
13770  line(start = [var 1, var 2], end = [var 3, var 4])
13771  line(start = [var 5, var 6], end = [var 7, var 8])
13772}
13773";
13774
13775        let program = Program::parse(initial_source).unwrap().0.unwrap();
13776
13777        let mut frontend = FrontendState::new();
13778
13779        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13780        let mock_ctx = ExecutorContext::new_mock(None).await;
13781        let version = Version(0);
13782
13783        frontend.hack_set_program(&ctx, program).await.unwrap();
13784        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13785        let sketch_id = sketch_object.id;
13786        let sketch = expect_sketch(sketch_object);
13787        let line1_id = *sketch.segments.get(2).unwrap();
13788        let line2_id = *sketch.segments.get(5).unwrap();
13789
13790        let constraint = Constraint::Perpendicular(Perpendicular {
13791            lines: vec![line1_id, line2_id],
13792        });
13793        let (src_delta, scene_delta) = frontend
13794            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13795            .await
13796            .unwrap();
13797        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13798        assert_eq!(
13799            scene_delta.new_graph.objects.len(),
13800            9,
13801            "{:#?}",
13802            scene_delta.new_graph.objects
13803        );
13804
13805        ctx.close().await;
13806        mock_ctx.close().await;
13807    }
13808
13809    #[tokio::test(flavor = "multi_thread")]
13810    async fn test_lines_angle() {
13811        let initial_source = "\
13812sketch(on = XY) {
13813  line(start = [var 1, var 2], end = [var 3, var 4])
13814  line(start = [var 5, var 6], end = [var 7, var 8])
13815}
13816";
13817
13818        let program = Program::parse(initial_source).unwrap().0.unwrap();
13819
13820        let mut frontend = FrontendState::new();
13821
13822        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13823        let mock_ctx = ExecutorContext::new_mock(None).await;
13824        let version = Version(0);
13825
13826        frontend.hack_set_program(&ctx, program).await.unwrap();
13827        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13828        let sketch_id = sketch_object.id;
13829        let sketch = expect_sketch(sketch_object);
13830        let line1_id = *sketch.segments.get(2).unwrap();
13831        let line2_id = *sketch.segments.get(5).unwrap();
13832
13833        let constraint = Constraint::Angle(Angle {
13834            lines: vec![line1_id, line2_id],
13835            angle: Number {
13836                value: 30.0,
13837                units: NumericSuffix::Deg,
13838            },
13839            sector: None,
13840            inverse: None,
13841            label_position: None,
13842            source: Default::default(),
13843        });
13844        let (src_delta, scene_delta) = frontend
13845            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13846            .await
13847            .unwrap();
13848        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13849        assert_eq!(
13850            scene_delta.new_graph.objects.len(),
13851            9,
13852            "{:#?}",
13853            scene_delta.new_graph.objects
13854        );
13855
13856        ctx.close().await;
13857        mock_ctx.close().await;
13858    }
13859
13860    #[tokio::test(flavor = "multi_thread")]
13861    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13862        let initial_source = "\
13863sketch(on = XY) {
13864  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13865  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13866}
13867";
13868
13869        let program = Program::parse(initial_source).unwrap().0.unwrap();
13870
13871        let mut frontend = FrontendState::new();
13872
13873        let mock_ctx = ExecutorContext::new_mock(None).await;
13874        let version = Version(0);
13875
13876        frontend.program = program.clone();
13877        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13878        frontend.update_state_after_exec(outcome, true);
13879        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13880        let sketch_id = sketch_object.id;
13881        let sketch = expect_sketch(sketch_object);
13882        let line1_id = *sketch.segments.get(2).unwrap();
13883        let line2_id = *sketch.segments.get(5).unwrap();
13884
13885        let constraint = Constraint::Angle(Angle {
13886            lines: vec![line1_id, line2_id],
13887            angle: Number {
13888                value: 270.0,
13889                units: NumericSuffix::Deg,
13890            },
13891            sector: Some(1),
13892            inverse: Some(true),
13893            label_position: Some(Point2d {
13894                x: Number {
13895                    value: -0.73,
13896                    units: NumericSuffix::Mm,
13897                },
13898                y: Number {
13899                    value: 0.75,
13900                    units: NumericSuffix::Mm,
13901                },
13902            }),
13903            source: Default::default(),
13904        });
13905        let (src_delta, _) = frontend
13906            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13907            .await
13908            .unwrap();
13909        assert_eq!(
13910            src_delta.text.as_str(),
13911            "\
13912sketch(on = XY) {
13913  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13914  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13915  angleDimension(
13916  lines = [line1, line2],
13917  sector = 1,
13918  inverse = true,
13919  labelPosition = [-0.73mm, 0.75mm],
13920) == 270deg
13921}
13922"
13923        );
13924
13925        mock_ctx.close().await;
13926    }
13927
13928    #[tokio::test(flavor = "multi_thread")]
13929    async fn test_segments_tangent() {
13930        let initial_source = "\
13931sketch(on = XY) {
13932  line(start = [var 1, var 2], end = [var 3, var 4])
13933  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13934}
13935";
13936
13937        let program = Program::parse(initial_source).unwrap().0.unwrap();
13938
13939        let mut frontend = FrontendState::new();
13940
13941        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13942        let mock_ctx = ExecutorContext::new_mock(None).await;
13943        let version = Version(0);
13944
13945        frontend.hack_set_program(&ctx, program).await.unwrap();
13946        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13947        let sketch_id = sketch_object.id;
13948        let sketch = expect_sketch(sketch_object);
13949        let line1_id = *sketch.segments.get(2).unwrap();
13950        let arc1_id = *sketch.segments.get(6).unwrap();
13951
13952        let constraint = Constraint::Tangent(Tangent {
13953            input: vec![line1_id, arc1_id],
13954        });
13955        let (src_delta, scene_delta) = frontend
13956            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13957            .await
13958            .unwrap();
13959        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13960        assert_eq!(
13961            scene_delta.new_graph.objects.len(),
13962            10,
13963            "{:#?}",
13964            scene_delta.new_graph.objects
13965        );
13966
13967        ctx.close().await;
13968        mock_ctx.close().await;
13969    }
13970
13971    #[tokio::test(flavor = "multi_thread")]
13972    async fn test_point_midpoint() {
13973        let initial_source = "\
13974sketch(on = XY) {
13975  point(at = [var 1, var 1])
13976  line(start = [var 0, var 0], end = [var 6, var 4])
13977}
13978";
13979
13980        let program = Program::parse(initial_source).unwrap().0.unwrap();
13981
13982        let mut frontend = FrontendState::new();
13983
13984        let ctx = ExecutorContext::new_mock(None).await;
13985        let version = Version(0);
13986
13987        frontend.program = program.clone();
13988        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13989        frontend.update_state_after_exec(outcome, true);
13990        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13991        let sketch_id = sketch_object.id;
13992        let sketch = expect_sketch(sketch_object);
13993        let point_id = *sketch.segments.first().unwrap();
13994        let line_id = *sketch.segments.get(3).unwrap();
13995
13996        let constraint = Constraint::Midpoint(Midpoint {
13997            point: ConstraintSegment::from(point_id),
13998            segment: line_id,
13999        });
14000        let (src_delta, scene_delta) = frontend
14001            .add_constraint(&ctx, version, sketch_id, constraint)
14002            .await
14003            .unwrap();
14004        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14005        assert_eq!(
14006            scene_delta.new_graph.objects.len(),
14007            7,
14008            "{:#?}",
14009            scene_delta.new_graph.objects
14010        );
14011
14012        ctx.close().await;
14013    }
14014
14015    #[tokio::test(flavor = "multi_thread")]
14016    async fn test_segments_symmetric() {
14017        let initial_source = "\
14018sketch(on = XY) {
14019  line(start = [var 0, var 0], end = [var 0, var 4])
14020  line(start = [var 4, var 0], end = [var 4, var 4])
14021  line(start = [var 2, var -1], end = [var 2, var 5])
14022}
14023";
14024
14025        let program = Program::parse(initial_source).unwrap().0.unwrap();
14026
14027        let mut frontend = FrontendState::new();
14028
14029        let ctx = ExecutorContext::new_mock(None).await;
14030        let version = Version(0);
14031
14032        frontend.program = program.clone();
14033        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14034        frontend.update_state_after_exec(outcome, true);
14035        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14036        let sketch_id = sketch_object.id;
14037        let sketch = expect_sketch(sketch_object);
14038        let line1_id = *sketch.segments.get(2).unwrap();
14039        let line2_id = *sketch.segments.get(5).unwrap();
14040        let axis_id = *sketch.segments.get(8).unwrap();
14041
14042        let constraint = Constraint::Symmetric(Symmetric {
14043            input: vec![line1_id, line2_id],
14044            axis: axis_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_segments_symmetric", src_delta.text.as_str());
14051        assert_eq!(
14052            scene_delta.new_graph.objects.len(),
14053            12,
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_point_arc_midpoint() {
14063        let initial_source = "\
14064sketch(on = XY) {
14065  point(at = [var 6, var 3])
14066  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14067}
14068";
14069
14070        let program = Program::parse(initial_source).unwrap().0.unwrap();
14071
14072        let mut frontend = FrontendState::new();
14073
14074        let ctx = ExecutorContext::new_mock(None).await;
14075        let version = Version(0);
14076
14077        frontend.program = program.clone();
14078        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14079        frontend.update_state_after_exec(outcome, true);
14080        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14081        let sketch_id = sketch_object.id;
14082        let sketch = expect_sketch(sketch_object);
14083        let point_id = *sketch.segments.first().unwrap();
14084        let arc_id = *sketch.segments.get(4).unwrap();
14085
14086        let constraint = Constraint::Midpoint(Midpoint {
14087            point: ConstraintSegment::from(point_id),
14088            segment: arc_id,
14089        });
14090        let (src_delta, scene_delta) = frontend
14091            .add_constraint(&ctx, version, sketch_id, constraint)
14092            .await
14093            .unwrap();
14094        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14095        assert_eq!(
14096            scene_delta.new_graph.objects.len(),
14097            8,
14098            "{:#?}",
14099            scene_delta.new_graph.objects
14100        );
14101
14102        ctx.close().await;
14103    }
14104
14105    #[tokio::test(flavor = "multi_thread")]
14106    async fn test_origin_line_midpoint() {
14107        let initial_source = "\
14108sketch(on = XY) {
14109  line(start = [var 0, var 0], end = [var 6, var 4])
14110}
14111";
14112
14113        let program = Program::parse(initial_source).unwrap().0.unwrap();
14114
14115        let mut frontend = FrontendState::new();
14116
14117        let ctx = ExecutorContext::new_mock(None).await;
14118        let version = Version(0);
14119
14120        frontend.program = program.clone();
14121        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14122        frontend.update_state_after_exec(outcome, true);
14123        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14124        let sketch_id = sketch_object.id;
14125        let sketch = expect_sketch(sketch_object);
14126        let line_id = *sketch.segments.get(2).unwrap();
14127
14128        let constraint = Constraint::Midpoint(Midpoint {
14129            point: ConstraintSegment::ORIGIN,
14130            segment: line_id,
14131        });
14132        let (src_delta, scene_delta) = frontend
14133            .add_constraint(&ctx, version, sketch_id, constraint)
14134            .await
14135            .unwrap();
14136        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14137        assert_eq!(
14138            scene_delta.new_graph.objects.len(),
14139            6,
14140            "{:#?}",
14141            scene_delta.new_graph.objects
14142        );
14143
14144        ctx.close().await;
14145    }
14146
14147    #[tokio::test(flavor = "multi_thread")]
14148    async fn test_origin_arc_midpoint() {
14149        let initial_source = "\
14150sketch(on = XY) {
14151  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14152}
14153";
14154
14155        let program = Program::parse(initial_source).unwrap().0.unwrap();
14156
14157        let mut frontend = FrontendState::new();
14158
14159        let ctx = ExecutorContext::new_mock(None).await;
14160        let version = Version(0);
14161
14162        frontend.program = program.clone();
14163        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14164        frontend.update_state_after_exec(outcome, true);
14165        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14166        let sketch_id = sketch_object.id;
14167        let sketch = expect_sketch(sketch_object);
14168        let arc_id = *sketch.segments.get(3).unwrap();
14169
14170        let constraint = Constraint::Midpoint(Midpoint {
14171            point: ConstraintSegment::ORIGIN,
14172            segment: arc_id,
14173        });
14174        let (src_delta, scene_delta) = frontend
14175            .add_constraint(&ctx, version, sketch_id, constraint)
14176            .await
14177            .unwrap();
14178        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14179        assert_eq!(
14180            scene_delta.new_graph.objects.len(),
14181            7,
14182            "{:#?}",
14183            scene_delta.new_graph.objects
14184        );
14185
14186        ctx.close().await;
14187    }
14188
14189    #[tokio::test(flavor = "multi_thread")]
14190    async fn test_segments_symmetric_arcs() {
14191        let initial_source = "\
14192sketch(on = XY) {
14193  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14194  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14195  line(start = [var 0, var -10], end = [var 0, var 10])
14196}
14197";
14198
14199        let program = Program::parse(initial_source).unwrap().0.unwrap();
14200
14201        let mut frontend = FrontendState::new();
14202
14203        let ctx = ExecutorContext::new_mock(None).await;
14204        let version = Version(0);
14205
14206        frontend.program = program.clone();
14207        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14208        frontend.update_state_after_exec(outcome, true);
14209        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14210        let sketch_id = sketch_object.id;
14211        let sketch = expect_sketch(sketch_object);
14212        let arc1_id = *sketch.segments.get(3).unwrap();
14213        let arc2_id = *sketch.segments.get(7).unwrap();
14214        let axis_id = *sketch.segments.get(10).unwrap();
14215
14216        let constraint = Constraint::Symmetric(Symmetric {
14217            input: vec![arc1_id, arc2_id],
14218            axis: axis_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_segments_symmetric_arcs", src_delta.text.as_str());
14225        assert_eq!(
14226            scene_delta.new_graph.objects.len(),
14227            14,
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_sketch_on_face_simple() {
14237        let initial_source = "\
14238len = 2mm
14239cube = startSketchOn(XY)
14240  |> startProfile(at = [0, 0])
14241  |> line(end = [len, 0], tag = $side)
14242  |> line(end = [0, len])
14243  |> line(end = [-len, 0])
14244  |> line(end = [0, -len])
14245  |> close()
14246  |> extrude(length = len)
14247
14248face = faceOf(cube, face = side)
14249";
14250
14251        let program = Program::parse(initial_source).unwrap().0.unwrap();
14252
14253        let mut frontend = FrontendState::new();
14254
14255        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14256        let mock_ctx = ExecutorContext::new_mock(None).await;
14257        let version = Version(0);
14258
14259        frontend.hack_set_program(&ctx, program).await.unwrap();
14260        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14261        let face_id = face_object.id;
14262
14263        let sketch_args = SketchCtor {
14264            on: Plane::Object(face_id),
14265        };
14266        let (_src_delta, scene_delta, sketch_id) = frontend
14267            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14268            .await
14269            .unwrap();
14270        assert_eq!(sketch_id, ObjectId(2));
14271        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14272        let sketch_object = &scene_delta.new_graph.objects[2];
14273        assert_eq!(sketch_object.id, ObjectId(2));
14274        assert_eq!(
14275            sketch_object.kind,
14276            ObjectKind::Sketch(Sketch {
14277                args: SketchCtor {
14278                    on: Plane::Object(face_id),
14279                },
14280                plane: face_id,
14281                segments: vec![],
14282                constraints: vec![],
14283            })
14284        );
14285        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14286
14287        ctx.close().await;
14288        mock_ctx.close().await;
14289    }
14290
14291    #[tokio::test(flavor = "multi_thread")]
14292    async fn test_new_sketch_on_primitive_index_face() {
14293        let initial_source = "\
14294@settings(kclVersion = 2.0)
14295
14296sketch001 = sketch(on = XY) {
14297  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14298}
14299extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14300shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14301        let program = Program::parse(initial_source).unwrap().0.unwrap();
14302        let ctx = ExecutorContext::new_mock(None).await;
14303        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14304        let solid_id = match outcome.variables.get("shell001") {
14305            Some(KclValueView::Solid { value }) => value.id,
14306            value => panic!("expected shell001 to be a solid, got {value:?}"),
14307        };
14308        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14309
14310        let mut ast = program.ast;
14311        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14312        let face_expr = sketch_on_ast_expr(
14313            &mut ast,
14314            &scene_graph,
14315            &solid_references,
14316            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14317        )
14318        .unwrap();
14319        let face_decl = ast::VariableDeclaration::new(
14320            ast::VariableDeclarator::new("face001", face_expr),
14321            ast::ItemVisibility::Default,
14322            ast::VariableKind::Const,
14323        );
14324        ast.body
14325            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14326                face_decl,
14327            ))));
14328        let face_source = source_from_ast(&ast);
14329        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14330        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14331
14332        let program = Program::parse(&new_source).unwrap().0.unwrap();
14333        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14334        ctx.close().await;
14335    }
14336
14337    #[tokio::test(flavor = "multi_thread")]
14338    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14339        let initial_source = "\
14340s = sketch(on = YZ) {
14341  line1 = line(start = [0, 0], end = [0, 1])
14342  line2 = line(start = [0, 1], end = [1, 1])
14343  line3 = line(start = [1, 1], end = [0, 0])
14344}
14345region001 = region(point = [0.1, 0.1], sketch = s)
14346extrude001 = extrude(region001, length = 5)
14347";
14348
14349        let program = Program::parse(initial_source).unwrap().0.unwrap();
14350
14351        let mut frontend = FrontendState::new();
14352        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14353        let version = Version(0);
14354
14355        frontend.hack_set_program(&ctx, program).await.unwrap();
14356        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14357
14358        let sketch_args = SketchCtor {
14359            on: Plane::Object(wall_object_id),
14360        };
14361        let (src_delta, _scene_delta, _sketch_id) = frontend
14362            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14363            .await
14364            .unwrap();
14365        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14366
14367        ctx.close().await;
14368    }
14369
14370    #[tokio::test(flavor = "multi_thread")]
14371    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14372        let initial_source = "\
14373sketch001 = sketch(on = YZ) {
14374  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14375  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14376  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14377  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14378  coincident([line1.end, line2.start])
14379  coincident([line2.end, line3.start])
14380  coincident([line3.end, line4.start])
14381  coincident([line4.end, line1.start])
14382  parallel([line2, line4])
14383  parallel([line3, line1])
14384  perpendicular([line1, line2])
14385  horizontal(line3)
14386  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14387}
14388region001 = region(point = [3.1, 3.74], sketch = sketch001)
14389extrude001 = extrude(region001, length = 5)
14390";
14391
14392        let program = Program::parse(initial_source).unwrap().0.unwrap();
14393
14394        let mut frontend = FrontendState::new();
14395        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14396        let version = Version(0);
14397
14398        frontend.hack_set_program(&ctx, program).await.unwrap();
14399        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14400
14401        let sketch_args = SketchCtor {
14402            on: Plane::Object(wall_object_id),
14403        };
14404        let (src_delta, _scene_delta, _sketch_id) = frontend
14405            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14406            .await
14407            .unwrap();
14408        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14409
14410        ctx.close().await;
14411    }
14412
14413    #[tokio::test(flavor = "multi_thread")]
14414    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14415        let initial_source = "\
14416@settings(kclVersion = 2.0)
14417
14418sketch001 = sketch(on = XY) {
14419  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14420  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14421}
14422hidden001 = hide(sketch001)
14423region001 = region(segments = [sketch001.circle2])
14424region002 = region(segments = [sketch001.circle1])
14425extrude001 = extrude([region001, region002], length = 5)
14426";
14427
14428        let program = Program::parse(initial_source).unwrap().0.unwrap();
14429        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14430        let version = Version(0);
14431
14432        for (solid_output_index, expected_face) in [
14433            (0, "faceOf(extrude001[0], face = END)"),
14434            (1, "faceOf(extrude001[1], face = END)"),
14435        ] {
14436            let mut frontend = FrontendState::new();
14437            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14438            let cap_object_id = find_cap_object_id_with_solid_output_index(
14439                &frontend.scene_graph,
14440                crate::frontend::api::CapKind::End,
14441                solid_output_index,
14442            )
14443            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14444
14445            let sketch_args = SketchCtor {
14446                on: Plane::Object(cap_object_id),
14447            };
14448            let (src_delta, _scene_delta, _sketch_id) = frontend
14449                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14450                .await
14451                .unwrap();
14452
14453            assert!(
14454                src_delta.text.contains(expected_face),
14455                "expected `{expected_face}` in:\n{}",
14456                src_delta.text
14457            );
14458            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14459        }
14460
14461        ctx.close().await;
14462    }
14463
14464    #[tokio::test(flavor = "multi_thread")]
14465    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14466        let initial_source = "\
14467@settings(kclVersion = 2.0)
14468
14469sketch001 = sketch(on = XY) {
14470  rect1Line1 = line(start = [0, 0], end = [1, 0])
14471  rect1Line2 = line(start = [1, 0], end = [1, 1])
14472  rect1Line3 = line(start = [1, 1], end = [0, 1])
14473  rect1Line4 = line(start = [0, 1], end = [0, 0])
14474  rect2Line1 = line(start = [3, 0], end = [4, 0])
14475  rect2Line2 = line(start = [4, 0], end = [4, 1])
14476  rect2Line3 = line(start = [4, 1], end = [3, 1])
14477  rect2Line4 = line(start = [3, 1], end = [3, 0])
14478}
14479hidden001 = hide(sketch001)
14480region001 = region(segments = [
14481  sketch001.rect1Line4,
14482  sketch001.rect1Line1
14483])
14484region002 = region(segments = [
14485  sketch001.rect2Line4,
14486  sketch001.rect2Line1
14487])
14488extrude001 = extrude([region001, region002], length = 5)
14489";
14490
14491        let program = Program::parse(initial_source).unwrap().0.unwrap();
14492        let mut frontend = FrontendState::new();
14493        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14494        let version = Version(0);
14495
14496        frontend.hack_set_program(&ctx, program).await.unwrap();
14497        let region_call = "\
14498region(segments = [
14499  sketch001.rect1Line4,
14500  sketch001.rect1Line1
14501])";
14502        let region_call_start = initial_source.find(region_call).unwrap();
14503        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14504        let segment_call = "line(start = [0, 0], end = [1, 0])";
14505        let segment_call_start = initial_source.find(segment_call).unwrap();
14506        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14507        let wall_object_id = frontend
14508            .scene_graph
14509            .objects
14510            .iter()
14511            .find_map(|object| match &object.kind {
14512                ObjectKind::Wall(wall)
14513                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14514                        && wall.source.segment.range == segment_range =>
14515                {
14516                    Some(object.id)
14517                }
14518                _ => None,
14519            })
14520            .expect("expected a wall object for region001.tags.rect1Line1");
14521
14522        let sketch_args = SketchCtor {
14523            on: Plane::Object(wall_object_id),
14524        };
14525        let (src_delta, _scene_delta, _sketch_id) = frontend
14526            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14527            .await
14528            .unwrap();
14529
14530        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14531        assert!(
14532            src_delta.text.contains(expected_face),
14533            "expected `{expected_face}` in:\n{}",
14534            src_delta.text
14535        );
14536        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14537
14538        ctx.close().await;
14539    }
14540
14541    #[test]
14542    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14543        let source = "\
14544sketch001 = sketch(on = XY) {
14545  line(start = [0, 0], end = [1, 0])
14546}
14547part = subtract(boxSolid, tools = [cutSolid])
14548  |> appearance(color = \"#8f96a3\")
14549";
14550        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14551        let line_start = source.find("line").unwrap();
14552        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14553        let line_ref = SourceRef::Simple {
14554            range: [line_start, line_end, 0].into(),
14555            node_path: None,
14556        };
14557        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14558
14559        let subtract_start = source.find("subtract").unwrap();
14560        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14561        let subtract_ref = SourceRef::Simple {
14562            range: [subtract_start, subtract_end, 0].into(),
14563            node_path: None,
14564        };
14565        assert_eq!(
14566            variable_name_containing_source_ref(&ast, &subtract_ref),
14567            Some("part".to_owned())
14568        );
14569    }
14570
14571    #[tokio::test(flavor = "multi_thread")]
14572    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14573        clear_mem_cache().await;
14574        let source = "\
14575boxSolid = startSketchOn(XY)
14576  |> startProfile(at = [0, 0])
14577  |> line(end = [4, 0], tag = $bottomEdge)
14578  |> line(end = [0, 4])
14579  |> line(end = [-4, 0])
14580  |> close()
14581  |> extrude(length = 10)
14582cutSolid = startSketchOn(XY)
14583  |> startProfile(at = [1, 1])
14584  |> line(end = [1, 0])
14585  |> line(end = [0, 1])
14586  |> line(end = [-1, 0])
14587  |> close()
14588  |> extrude(length = 10)
14589part = subtract(boxSolid, tools = [cutSolid])
14590  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14591";
14592        let program = Program::parse(source).unwrap().0.unwrap();
14593        let mut frontend = FrontendState::new();
14594        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14595        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14596            SetProgramOutcome::Success { .. } => {}
14597            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14598        }
14599
14600        let sweep_call_start = source.find("extrude").unwrap();
14601        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14602        let part_call_start = source.find("subtract").unwrap();
14603        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14604        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14605        let composite_range = [part_call_start, part_call_end, 0].into();
14606
14607        let cap_object = frontend
14608            .scene_graph
14609            .objects
14610            .iter()
14611            .find(|object| {
14612                matches!(
14613                    &object.kind,
14614                    ObjectKind::Cap(crate::frontend::api::Cap {
14615                        kind: crate::frontend::api::CapKind::End,
14616                        source,
14617                        ..
14618                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14619                )
14620            })
14621            .expect("expected end cap object to trace through subtract and original extrude");
14622
14623        let mut ast = frontend.program.ast.clone();
14624        let cap_expr = sketch_on_ast_expr(
14625            &mut ast,
14626            &frontend.scene_graph,
14627            &frontend.solid_references,
14628            &Plane::Object(cap_object.id),
14629        )
14630        .unwrap();
14631        let cap_face_decl = ast::VariableDeclaration::new(
14632            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14633            ast::ItemVisibility::Default,
14634            ast::VariableKind::Const,
14635        );
14636        ast.body
14637            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14638                cap_face_decl,
14639            ))));
14640        let generated_source = source_from_ast(&ast);
14641
14642        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14643        assert!(!generated_source.contains("faceOf(boxSolid"));
14644        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14645            panic!("expected faceOf call");
14646        };
14647        assert_eq!(call.callee.name.name, "faceOf");
14648        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14649            panic!("expected solid name");
14650        };
14651        assert_eq!(solid_name.name.name, "part");
14652        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14653            panic!("expected face name");
14654        };
14655        assert_eq!(face_name.name.name, "END");
14656
14657        ctx.close().await;
14658    }
14659
14660    #[tokio::test(flavor = "multi_thread")]
14661    async fn test_sketch_on_plane_incremental() {
14662        let initial_source = "\
14663len = 2mm
14664cube = startSketchOn(XY)
14665  |> startProfile(at = [0, 0])
14666  |> line(end = [len, 0], tag = $side)
14667  |> line(end = [0, len])
14668  |> line(end = [-len, 0])
14669  |> line(end = [0, -len])
14670  |> close()
14671  |> extrude(length = len)
14672
14673plane = planeOf(cube, face = side)
14674";
14675
14676        let program = Program::parse(initial_source).unwrap().0.unwrap();
14677
14678        let mut frontend = FrontendState::new();
14679
14680        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14681        let mock_ctx = ExecutorContext::new_mock(None).await;
14682        let version = Version(0);
14683
14684        frontend.hack_set_program(&ctx, program).await.unwrap();
14685        // Find the last plane since the first plane is the XY plane.
14686        let plane_object = frontend
14687            .scene_graph
14688            .objects
14689            .iter()
14690            .rev()
14691            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14692            .unwrap();
14693        let plane_id = plane_object.id;
14694
14695        let sketch_args = SketchCtor {
14696            on: Plane::Object(plane_id),
14697        };
14698        let (src_delta, scene_delta, sketch_id) = frontend
14699            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14700            .await
14701            .unwrap();
14702        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14703        assert_eq!(sketch_id, ObjectId(2));
14704        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14705        let sketch_object = &scene_delta.new_graph.objects[2];
14706        assert_eq!(sketch_object.id, ObjectId(2));
14707        assert_eq!(
14708            sketch_object.kind,
14709            ObjectKind::Sketch(Sketch {
14710                args: SketchCtor {
14711                    on: Plane::Object(plane_id),
14712                },
14713                plane: plane_id,
14714                segments: vec![],
14715                constraints: vec![],
14716            })
14717        );
14718        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14719
14720        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14721        assert_eq!(plane_object.id, plane_id);
14722        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14723
14724        ctx.close().await;
14725        mock_ctx.close().await;
14726    }
14727
14728    #[tokio::test(flavor = "multi_thread")]
14729    async fn test_new_sketch_uses_unique_variable_name() {
14730        let initial_source = "\
14731sketch1 = sketch(on = XY) {
14732}
14733";
14734
14735        let program = Program::parse(initial_source).unwrap().0.unwrap();
14736
14737        let mut frontend = FrontendState::new();
14738        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14739        let version = Version(0);
14740
14741        frontend.hack_set_program(&ctx, program).await.unwrap();
14742
14743        let sketch_args = SketchCtor {
14744            on: Plane::Default(PlaneName::Yz),
14745        };
14746        let (src_delta, _, _) = frontend
14747            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14748            .await
14749            .unwrap();
14750
14751        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14752
14753        ctx.close().await;
14754    }
14755
14756    #[tokio::test(flavor = "multi_thread")]
14757    async fn test_new_sketch_twice_using_same_plane() {
14758        let initial_source = "\
14759sketch1 = sketch(on = XY) {
14760}
14761";
14762
14763        let program = Program::parse(initial_source).unwrap().0.unwrap();
14764
14765        let mut frontend = FrontendState::new();
14766        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14767        let version = Version(0);
14768
14769        frontend.hack_set_program(&ctx, program).await.unwrap();
14770
14771        let sketch_args = SketchCtor {
14772            on: Plane::Default(PlaneName::Xy),
14773        };
14774        let (src_delta, _, _) = frontend
14775            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14776            .await
14777            .unwrap();
14778
14779        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14780
14781        ctx.close().await;
14782    }
14783
14784    #[tokio::test(flavor = "multi_thread")]
14785    async fn test_sketch_mode_reuses_cached_on_expression() {
14786        let initial_source = "\
14787width = 2mm
14788sketch(on = offsetPlane(XY, offset = width)) {
14789  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14790  distance([line1.start, line1.end]) == width
14791}
14792";
14793        let program = Program::parse(initial_source).unwrap().0.unwrap();
14794
14795        let mut frontend = FrontendState::new();
14796        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14797        let mock_ctx = ExecutorContext::new_mock(None).await;
14798        let version = Version(0);
14799        let project_id = ProjectId(0);
14800        let file_id = FileId(0);
14801
14802        frontend.hack_set_program(&ctx, program).await.unwrap();
14803        let initial_object_count = frontend.scene_graph.objects.len();
14804        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14805            .expect("Expected sketch object to exist")
14806            .id;
14807
14808        // Entering sketch mode should reuse cached `on` expression state
14809        // (offsetPlane result), not fail or create extra on-surface objects.
14810        let scene_delta = frontend
14811            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14812            .await
14813            .unwrap();
14814        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14815
14816        // A follow-up sketch-mode execution should keep the same stable object
14817        // graph shape as well.
14818        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14819        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14820
14821        ctx.close().await;
14822        mock_ctx.close().await;
14823    }
14824
14825    #[tokio::test(flavor = "multi_thread")]
14826    async fn test_edit_sketch_nested_in_pipe() {
14827        clear_mem_cache().await;
14828        let source = r#"
14829profile = sketch(on = XY) {
14830  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14831}
14832  |> translate(x = 2mm)
14833"#;
14834        let program = Program::parse_no_errs(source).unwrap();
14835        let mut frontend = FrontendState::new();
14836        let mock_ctx = ExecutorContext::new_mock(None).await;
14837        let version = Version(0);
14838
14839        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14840        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14841            .expect("Expected piped sketch object")
14842            .id;
14843
14844        let scene_delta = frontend
14845            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14846            .await
14847            .unwrap();
14848        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14849        assert!(
14850            scene_delta
14851                .new_graph
14852                .objects
14853                .iter()
14854                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14855            "Expected the piped sketch's segments to be present in sketch mode"
14856        );
14857
14858        clear_mem_cache().await;
14859        mock_ctx.close().await;
14860    }
14861
14862    #[tokio::test(flavor = "multi_thread")]
14863    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14864        clear_mem_cache().await;
14865        let source = r#"
14866useFirstProfile = true
14867
14868profile = if useFirstProfile {
14869  sketch(on = XY) {
14870    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14871  }
14872} else {
14873  sketch(on = XY) {
14874    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14875  }
14876}
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_object =
14885            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14886        let sketch_id = sketch_object.id;
14887        let sketch = expect_sketch(sketch_object);
14888        let line_end_id = *sketch
14889            .segments
14890            .get(1)
14891            .expect("Expected the active branch's line end point");
14892
14893        let scene_delta = frontend
14894            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14895            .await
14896            .unwrap();
14897        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14898
14899        let segments = vec![ExistingSegmentCtor {
14900            id: line_end_id,
14901            ctor: SegmentCtor::Point(PointCtor {
14902                position: Point2d {
14903                    x: Expr::Var(Number {
14904                        value: 30.0,
14905                        units: NumericSuffix::Mm,
14906                    }),
14907                    y: Expr::Var(Number {
14908                        value: 15.0,
14909                        units: NumericSuffix::Mm,
14910                    }),
14911                },
14912            }),
14913        }];
14914        let (source_delta, _) = frontend
14915            .edit_segments(&mock_ctx, version, sketch_id, segments)
14916            .await
14917            .unwrap();
14918        assert!(
14919            source_delta
14920                .text
14921                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14922            "Expected the active branch's dragged variables to be updated:\n{}",
14923            source_delta.text
14924        );
14925        assert!(
14926            source_delta
14927                .text
14928                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14929            "Expected the inactive branch to remain unchanged:\n{}",
14930            source_delta.text
14931        );
14932
14933        clear_mem_cache().await;
14934        mock_ctx.close().await;
14935    }
14936
14937    #[tokio::test(flavor = "multi_thread")]
14938    async fn test_multiple_sketch_blocks() {
14939        let initial_source = "\
14940// Cube that requires the engine.
14941width = 2
14942sketch001 = startSketchOn(XY)
14943profile001 = startProfile(sketch001, at = [0, 0])
14944  |> yLine(length = width, tag = $seg1)
14945  |> xLine(length = width)
14946  |> yLine(length = -width)
14947  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14948  |> close()
14949extrude001 = extrude(profile001, length = width)
14950
14951// Get a value that requires the engine.
14952x = segLen(seg1)
14953
14954// Triangle with side length 2*x.
14955sketch(on = XY) {
14956  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14957  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14958  coincident([line1.end, line2.start])
14959  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14960  coincident([line2.end, line3.start])
14961  coincident([line3.end, line1.start])
14962  equalLength([line3, line1])
14963  equalLength([line1, line2])
14964  distance([line1.start, line1.end]) == 2*x
14965}
14966
14967// Line segment with length x.
14968sketch2 = sketch(on = XY) {
14969  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14970  distance([line1.start, line1.end]) == x
14971}
14972";
14973
14974        let program = Program::parse(initial_source).unwrap().0.unwrap();
14975
14976        let mut frontend = FrontendState::new();
14977
14978        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14979        let mock_ctx = ExecutorContext::new_mock(None).await;
14980        let version = Version(0);
14981        let project_id = ProjectId(0);
14982        let file_id = FileId(0);
14983
14984        frontend.hack_set_program(&ctx, program).await.unwrap();
14985        let sketch_objects = frontend
14986            .scene_graph
14987            .objects
14988            .iter()
14989            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14990            .collect::<Vec<_>>();
14991        let sketch1_id = sketch_objects.first().unwrap().id;
14992        let sketch2_id = sketch_objects.get(1).unwrap().id;
14993        // First point in sketch1.
14994        let point1_id = ObjectId(sketch1_id.0 + 1);
14995        // First point in sketch2.
14996        let point2_id = ObjectId(sketch2_id.0 + 1);
14997
14998        // Edit the first sketch. Objects before the sketch block should be
14999        // present from execution cache so that we can sketch on prior planes,
15000        // for example. Objects after the first sketch block should not be
15001        // present since those statements are skipped in sketch mode.
15002        //
15003        // - startSketchOn(XY) Plane 1
15004        // - sketch on=XY Plane 1
15005        // - Sketch block 16
15006        let scene_delta = frontend
15007            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15008            .await
15009            .unwrap();
15010        assert_eq!(
15011            scene_delta.new_graph.objects.len(),
15012            18,
15013            "{:#?}",
15014            scene_delta.new_graph.objects
15015        );
15016
15017        // Edit a point in the first sketch.
15018        let point_ctor = PointCtor {
15019            position: Point2d {
15020                x: Expr::Var(Number {
15021                    value: 1.0,
15022                    units: NumericSuffix::Mm,
15023                }),
15024                y: Expr::Var(Number {
15025                    value: 2.0,
15026                    units: NumericSuffix::Mm,
15027                }),
15028            },
15029        };
15030        let segments = vec![ExistingSegmentCtor {
15031            id: point1_id,
15032            ctor: SegmentCtor::Point(point_ctor),
15033        }];
15034        let (src_delta, _) = frontend
15035            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15036            .await
15037            .unwrap();
15038        // Only the first sketch block changes.
15039        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15040        let edited_sketch1_source = src_delta.text.clone();
15041
15042        // Execute mock to simulate drag end.
15043        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15044        assert_eq!(src_delta.text, edited_sketch1_source);
15045        // Exit sketch. Objects from the entire program should be present.
15046        //
15047        // - startSketchOn(XY) Plane 1
15048        // - sketch on=XY Plane 1
15049        // - Sketch block 16
15050        // - sketch on=XY cached
15051        // - Sketch block 5
15052        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15053        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15054
15055        // Edit the second sketch.
15056        //
15057        // - startSketchOn(XY) Plane 1
15058        // - sketch on=XY Plane 1
15059        // - Sketch block 16
15060        // - sketch on=XY cached
15061        // - Sketch block 5
15062        let scene_delta = frontend
15063            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15064            .await
15065            .unwrap();
15066        assert_eq!(
15067            scene_delta.new_graph.objects.len(),
15068            24,
15069            "{:#?}",
15070            scene_delta.new_graph.objects
15071        );
15072
15073        // Edit a point in the second sketch.
15074        let point_ctor = PointCtor {
15075            position: Point2d {
15076                x: Expr::Var(Number {
15077                    value: 3.0,
15078                    units: NumericSuffix::Mm,
15079                }),
15080                y: Expr::Var(Number {
15081                    value: 4.0,
15082                    units: NumericSuffix::Mm,
15083                }),
15084            },
15085        };
15086        let segments = vec![ExistingSegmentCtor {
15087            id: point2_id,
15088            ctor: SegmentCtor::Point(point_ctor),
15089        }];
15090        let (src_delta, _) = frontend
15091            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15092            .await
15093            .unwrap();
15094        // Only the second sketch block changes.
15095        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15096        let edited_sketch2_source = src_delta.text.clone();
15097
15098        // Execute mock to simulate drag end.
15099        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15100        assert_eq!(src_delta.text, edited_sketch2_source);
15101
15102        ctx.close().await;
15103        mock_ctx.close().await;
15104    }
15105
15106    #[tokio::test(flavor = "multi_thread")]
15107    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15108        clear_mem_cache().await;
15109
15110        let source = r#"sketch001 = sketch(on = XZ) {
15111  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15112}
15113sketch002 = sketch(on = XY) {
15114  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15115  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15116  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15117  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15118  coincident([line1.end, line2.start])
15119  coincident([line2.end, line3.start])
15120  coincident([line3.end, line4.start])
15121  coincident([line4.end, line1.start])
15122  parallel([line2, line4])
15123  parallel([line3, line1])
15124  perpendicular([line1, line2])
15125  horizontal(line3)
15126  coincident([line1.start, ORIGIN])
15127}
15128"#;
15129
15130        let program = Program::parse(source).unwrap().0.unwrap();
15131        let mut frontend = FrontendState::new();
15132        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15133        let mock_ctx = ExecutorContext::new_mock(None).await;
15134        let version = Version(0);
15135        let project_id = ProjectId(0);
15136        let file_id = FileId(0);
15137
15138        frontend.hack_set_program(&ctx, program).await.unwrap();
15139        let sketch_objects = frontend
15140            .scene_graph
15141            .objects
15142            .iter()
15143            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15144            .collect::<Vec<_>>();
15145        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15146
15147        let sketch1_id = sketch_objects[0].id;
15148        let sketch2_id = sketch_objects[1].id;
15149
15150        frontend
15151            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15152            .await
15153            .unwrap();
15154        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15155
15156        let scene_delta = frontend
15157            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15158            .await
15159            .unwrap();
15160        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15161
15162        clear_mem_cache().await;
15163        ctx.close().await;
15164        mock_ctx.close().await;
15165    }
15166
15167    // Regression tests: operations on source code with extra whitespace/newlines.
15168    // These test that NodePath-based lookups work correctly when source ranges
15169    // are shifted by extra whitespace that wouldn't be present after formatting.
15170
15171    #[tokio::test(flavor = "multi_thread")]
15172    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15173        // Extra newlines after @settings line - this shifts all source ranges.
15174        let initial_source = "@settings(defaultLengthUnit = mm)
15175
15176sketch001 = sketch(on = XY) {
15177  point(at = [1in, 2in])
15178}
15179";
15180
15181        let program = Program::parse(initial_source).unwrap().0.unwrap();
15182        let mut frontend = FrontendState::new();
15183
15184        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15185        let mock_ctx = ExecutorContext::new_mock(None).await;
15186        let version = Version(0);
15187        let project_id = ProjectId(0);
15188        let file_id = FileId(0);
15189
15190        frontend.hack_set_program(&ctx, program).await.unwrap();
15191        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15192        let sketch_id = sketch_object.id;
15193
15194        // Edit sketch should succeed despite extra newlines.
15195        frontend
15196            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15197            .await
15198            .unwrap();
15199
15200        // Add a new point to the sketch.
15201        let point_ctor = PointCtor {
15202            position: Point2d {
15203                x: Expr::Number(Number {
15204                    value: 5.0,
15205                    units: NumericSuffix::Mm,
15206                }),
15207                y: Expr::Number(Number {
15208                    value: 6.0,
15209                    units: NumericSuffix::Mm,
15210                }),
15211            },
15212        };
15213        let segment = SegmentCtor::Point(point_ctor);
15214        let (src_delta, scene_delta) = frontend
15215            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15216            .await
15217            .unwrap();
15218        // After adding a point, the source should be reformatted with standard whitespace.
15219        assert!(
15220            src_delta.text.contains("point(at = [5mm, 6mm])"),
15221            "Expected new point in source, got: {}",
15222            src_delta.text
15223        );
15224        assert!(!scene_delta.new_objects.is_empty());
15225
15226        ctx.close().await;
15227        mock_ctx.close().await;
15228    }
15229
15230    #[tokio::test(flavor = "multi_thread")]
15231    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15232        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15233
15234        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15235        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15236
15237        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15238        assert!(
15239            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15240            "Expected experimental settings to be added to source"
15241        );
15242    }
15243
15244    #[tokio::test(flavor = "multi_thread")]
15245    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15246        // Extra newlines after @settings, with an empty sketch block.
15247        let initial_source = "@settings(defaultLengthUnit = mm)
15248
15249s = sketch(on = XY) {}
15250";
15251
15252        let program = Program::parse(initial_source).unwrap().0.unwrap();
15253        let mut frontend = FrontendState::new();
15254
15255        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15256        let mock_ctx = ExecutorContext::new_mock(None).await;
15257        let version = Version(0);
15258
15259        frontend.hack_set_program(&ctx, program).await.unwrap();
15260        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15261        let sketch_id = sketch_object.id;
15262
15263        let line_ctor = LineCtor {
15264            start: Point2d {
15265                x: Expr::Number(Number {
15266                    value: 0.0,
15267                    units: NumericSuffix::Mm,
15268                }),
15269                y: Expr::Number(Number {
15270                    value: 0.0,
15271                    units: NumericSuffix::Mm,
15272                }),
15273            },
15274            end: Point2d {
15275                x: Expr::Number(Number {
15276                    value: 10.0,
15277                    units: NumericSuffix::Mm,
15278                }),
15279                y: Expr::Number(Number {
15280                    value: 10.0,
15281                    units: NumericSuffix::Mm,
15282                }),
15283            },
15284            construction: None,
15285        };
15286        let segment = SegmentCtor::Line(line_ctor);
15287        let (src_delta, scene_delta) = frontend
15288            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15289            .await
15290            .unwrap();
15291        assert!(
15292            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15293            "Expected line in source, got: {}",
15294            src_delta.text
15295        );
15296        // Line creates start point, end point, and line segment.
15297        assert_eq!(scene_delta.new_objects.len(), 3);
15298
15299        ctx.close().await;
15300        mock_ctx.close().await;
15301    }
15302
15303    #[tokio::test(flavor = "multi_thread")]
15304    async fn test_extra_newlines_between_operations_edit_line() {
15305        // Extra newlines between @settings and sketch, and inside the sketch block.
15306        let initial_source = "@settings(defaultLengthUnit = mm)
15307
15308sketch001 = sketch(on = XY) {
15309
15310  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15311
15312}
15313";
15314
15315        let program = Program::parse(initial_source).unwrap().0.unwrap();
15316        let mut frontend = FrontendState::new();
15317
15318        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15319        let mock_ctx = ExecutorContext::new_mock(None).await;
15320        let version = Version(0);
15321        let project_id = ProjectId(0);
15322        let file_id = FileId(0);
15323
15324        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15325        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15326        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15327        let sketch_id = sketch_object.id;
15328        let sketch = expect_sketch(sketch_object);
15329
15330        // Extract segment IDs before edit_sketch borrows frontend mutably.
15331        let line_id = sketch
15332            .segments
15333            .iter()
15334            .copied()
15335            .find(|seg_id| {
15336                matches!(
15337                    &frontend.scene_graph.objects[seg_id.0].kind,
15338                    ObjectKind::Segment {
15339                        segment: Segment::Line(_)
15340                    }
15341                )
15342            })
15343            .expect("Expected a line segment in sketch");
15344
15345        // Enter sketch edit mode.
15346        frontend
15347            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15348            .await
15349            .unwrap();
15350
15351        // Edit the line.
15352        let line_ctor = LineCtor {
15353            start: Point2d {
15354                x: Expr::Var(Number {
15355                    value: 1.0,
15356                    units: NumericSuffix::Mm,
15357                }),
15358                y: Expr::Var(Number {
15359                    value: 2.0,
15360                    units: NumericSuffix::Mm,
15361                }),
15362            },
15363            end: Point2d {
15364                x: Expr::Var(Number {
15365                    value: 13.0,
15366                    units: NumericSuffix::Mm,
15367                }),
15368                y: Expr::Var(Number {
15369                    value: 14.0,
15370                    units: NumericSuffix::Mm,
15371                }),
15372            },
15373            construction: None,
15374        };
15375        let segments = vec![ExistingSegmentCtor {
15376            id: line_id,
15377            ctor: SegmentCtor::Line(line_ctor),
15378        }];
15379        let (src_delta, _scene_delta) = frontend
15380            .edit_segments(&mock_ctx, version, sketch_id, segments)
15381            .await
15382            .unwrap();
15383        assert!(
15384            src_delta
15385                .text
15386                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15387            "Expected edited line in source, got: {}",
15388            src_delta.text
15389        );
15390
15391        ctx.close().await;
15392        mock_ctx.close().await;
15393    }
15394
15395    #[tokio::test(flavor = "multi_thread")]
15396    async fn test_extra_newlines_delete_segment() {
15397        // Extra whitespace before and after the sketch block.
15398        let initial_source = "@settings(defaultLengthUnit = mm)
15399
15400sketch001 = sketch(on = XY) {
15401  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15402}
15403";
15404
15405        let program = Program::parse(initial_source).unwrap().0.unwrap();
15406        let mut frontend = FrontendState::new();
15407
15408        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15409        let mock_ctx = ExecutorContext::new_mock(None).await;
15410        let version = Version(0);
15411
15412        frontend.hack_set_program(&ctx, program).await.unwrap();
15413        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15414        let sketch_id = sketch_object.id;
15415        let sketch = expect_sketch(sketch_object);
15416
15417        // The sketch should have 3 segments: start point, center point, and the circle.
15418        assert_eq!(sketch.segments.len(), 3);
15419        let circle_id = sketch.segments[2];
15420
15421        // Delete the circle despite extra newlines in original source.
15422        let (src_delta, scene_delta) = frontend
15423            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15424            .await
15425            .unwrap();
15426        assert!(
15427            src_delta.text.contains("sketch(on = XY) {"),
15428            "Expected sketch block in source, got: {}",
15429            src_delta.text
15430        );
15431        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15432        let new_sketch = expect_sketch(new_sketch_object);
15433        assert_eq!(new_sketch.segments.len(), 0);
15434
15435        ctx.close().await;
15436        mock_ctx.close().await;
15437    }
15438
15439    #[tokio::test(flavor = "multi_thread")]
15440    async fn test_unformatted_source_add_arc() {
15441        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15442        let initial_source = "@settings(defaultLengthUnit = mm)
15443
15444sketch001 = sketch(on = XY) {
15445}
15446";
15447
15448        let program = Program::parse(initial_source).unwrap().0.unwrap();
15449        let mut frontend = FrontendState::new();
15450
15451        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15452        let mock_ctx = ExecutorContext::new_mock(None).await;
15453        let version = Version(0);
15454
15455        frontend.hack_set_program(&ctx, program).await.unwrap();
15456        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15457        let sketch_id = sketch_object.id;
15458
15459        let arc_ctor = ArcCtor {
15460            start: Point2d {
15461                x: Expr::Var(Number {
15462                    value: 5.0,
15463                    units: NumericSuffix::Mm,
15464                }),
15465                y: Expr::Var(Number {
15466                    value: 0.0,
15467                    units: NumericSuffix::Mm,
15468                }),
15469            },
15470            end: Point2d {
15471                x: Expr::Var(Number {
15472                    value: 0.0,
15473                    units: NumericSuffix::Mm,
15474                }),
15475                y: Expr::Var(Number {
15476                    value: 5.0,
15477                    units: NumericSuffix::Mm,
15478                }),
15479            },
15480            center: Point2d {
15481                x: Expr::Var(Number {
15482                    value: 0.0,
15483                    units: NumericSuffix::Mm,
15484                }),
15485                y: Expr::Var(Number {
15486                    value: 0.0,
15487                    units: NumericSuffix::Mm,
15488                }),
15489            },
15490            direction: None,
15491            construction: None,
15492        };
15493        let segment = SegmentCtor::Arc(arc_ctor);
15494        let (src_delta, scene_delta) = frontend
15495            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15496            .await
15497            .unwrap();
15498        assert!(
15499            src_delta
15500                .text
15501                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15502            "Expected arc in source, got: {}",
15503            src_delta.text
15504        );
15505        assert!(!scene_delta.new_objects.is_empty());
15506
15507        ctx.close().await;
15508        mock_ctx.close().await;
15509    }
15510
15511    #[tokio::test(flavor = "multi_thread")]
15512    async fn test_arc_direction_flows_to_source() {
15513        let initial_source = "@settings(defaultLengthUnit = mm)
15514
15515sketch001 = sketch(on = XY) {
15516}
15517";
15518
15519        let program = Program::parse(initial_source).unwrap().0.unwrap();
15520        let mut frontend = FrontendState::new();
15521
15522        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15523        let mock_ctx = ExecutorContext::new_mock(None).await;
15524        let version = Version(0);
15525
15526        frontend.hack_set_program(&ctx, program).await.unwrap();
15527        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15528        let sketch_id = sketch_object.id;
15529
15530        let point = |x: f64, y: f64| Point2d {
15531            x: Expr::Var(Number {
15532                value: x,
15533                units: NumericSuffix::Mm,
15534            }),
15535            y: Expr::Var(Number {
15536                value: y,
15537                units: NumericSuffix::Mm,
15538            }),
15539        };
15540
15541        // Adding a clockwise arc writes its direction to the source.
15542        let arc_ctor = ArcCtor {
15543            start: point(5.0, 0.0),
15544            end: point(0.0, 5.0),
15545            center: point(0.0, 0.0),
15546            direction: Some(ArcDirection::Cw),
15547            construction: None,
15548        };
15549        let (src_delta, scene_delta) = frontend
15550            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15551            .await
15552            .unwrap();
15553        assert!(
15554            src_delta.text.contains("direction = CW"),
15555            "Expected direction = CW in source, got: {}",
15556            src_delta.text
15557        );
15558        // The new objects are the end points, the center, and then the arc.
15559        let arc_id = *scene_delta.new_objects.last().unwrap();
15560
15561        // Editing the arc's points preserves the clockwise direction. The
15562        // edited points keep the same distance to the center so that the
15563        // solver doesn't need to move anything.
15564        let edited_ctor = ArcCtor {
15565            start: point(0.0, -5.0),
15566            end: point(0.0, 5.0),
15567            center: point(0.0, 0.0),
15568            direction: Some(ArcDirection::Cw),
15569            construction: None,
15570        };
15571        let (src_delta, _scene_delta) = frontend
15572            .edit_segments(
15573                &mock_ctx,
15574                version,
15575                sketch_id,
15576                vec![ExistingSegmentCtor {
15577                    id: arc_id,
15578                    ctor: SegmentCtor::Arc(edited_ctor),
15579                }],
15580            )
15581            .await
15582            .unwrap();
15583        assert!(
15584            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15585            "Expected edited start point in source, got: {}",
15586            src_delta.text
15587        );
15588        assert!(
15589            src_delta.text.contains("direction = CW"),
15590            "Expected direction = CW to be preserved in source, got: {}",
15591            src_delta.text
15592        );
15593
15594        // Editing the arc back to counterclockwise removes the direction
15595        // argument since counterclockwise is the default.
15596        let edited_ctor = ArcCtor {
15597            start: point(0.0, -5.0),
15598            end: point(0.0, 5.0),
15599            center: point(0.0, 0.0),
15600            direction: Some(ArcDirection::Ccw),
15601            construction: None,
15602        };
15603        let (src_delta, _scene_delta) = frontend
15604            .edit_segments(
15605                &mock_ctx,
15606                version,
15607                sketch_id,
15608                vec![ExistingSegmentCtor {
15609                    id: arc_id,
15610                    ctor: SegmentCtor::Arc(edited_ctor),
15611                }],
15612            )
15613            .await
15614            .unwrap();
15615        assert!(
15616            !src_delta.text.contains("direction"),
15617            "Expected direction argument to be removed from source, got: {}",
15618            src_delta.text
15619        );
15620
15621        ctx.close().await;
15622        mock_ctx.close().await;
15623    }
15624
15625    #[tokio::test(flavor = "multi_thread")]
15626    async fn test_extra_newlines_add_circle() {
15627        // Extra blank lines between settings and sketch.
15628        let initial_source = "@settings(defaultLengthUnit = mm)
15629
15630sketch001 = sketch(on = XY) {
15631}
15632";
15633
15634        let program = Program::parse(initial_source).unwrap().0.unwrap();
15635        let mut frontend = FrontendState::new();
15636
15637        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15638        let mock_ctx = ExecutorContext::new_mock(None).await;
15639        let version = Version(0);
15640
15641        frontend.hack_set_program(&ctx, program).await.unwrap();
15642        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15643        let sketch_id = sketch_object.id;
15644
15645        let circle_ctor = CircleCtor {
15646            start: Point2d {
15647                x: Expr::Var(Number {
15648                    value: 5.0,
15649                    units: NumericSuffix::Mm,
15650                }),
15651                y: Expr::Var(Number {
15652                    value: 0.0,
15653                    units: NumericSuffix::Mm,
15654                }),
15655            },
15656            center: Point2d {
15657                x: Expr::Var(Number {
15658                    value: 0.0,
15659                    units: NumericSuffix::Mm,
15660                }),
15661                y: Expr::Var(Number {
15662                    value: 0.0,
15663                    units: NumericSuffix::Mm,
15664                }),
15665            },
15666            construction: None,
15667        };
15668        let segment = SegmentCtor::Circle(circle_ctor);
15669        let (src_delta, scene_delta) = frontend
15670            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15671            .await
15672            .unwrap();
15673        assert!(
15674            src_delta
15675                .text
15676                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15677            "Expected circle in source, got: {}",
15678            src_delta.text
15679        );
15680        assert!(!scene_delta.new_objects.is_empty());
15681
15682        ctx.close().await;
15683        mock_ctx.close().await;
15684    }
15685
15686    #[tokio::test(flavor = "multi_thread")]
15687    async fn test_extra_newlines_add_constraint() {
15688        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15689        let initial_source = "@settings(defaultLengthUnit = mm)
15690
15691sketch001 = sketch(on = XY) {
15692  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15693  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15694}
15695";
15696
15697        let program = Program::parse(initial_source).unwrap().0.unwrap();
15698        let mut frontend = FrontendState::new();
15699
15700        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15701        let mock_ctx = ExecutorContext::new_mock(None).await;
15702        let version = Version(0);
15703        let project_id = ProjectId(0);
15704        let file_id = FileId(0);
15705
15706        frontend.hack_set_program(&ctx, program).await.unwrap();
15707        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15708        let sketch_id = sketch_object.id;
15709        let sketch = expect_sketch(sketch_object);
15710
15711        // Extract segment data before edit_sketch borrows frontend mutably.
15712        let line_ids: Vec<ObjectId> = sketch
15713            .segments
15714            .iter()
15715            .copied()
15716            .filter(|seg_id| {
15717                matches!(
15718                    &frontend.scene_graph.objects[seg_id.0].kind,
15719                    ObjectKind::Segment {
15720                        segment: Segment::Line(_)
15721                    }
15722                )
15723            })
15724            .collect();
15725        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15726
15727        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15728        let ObjectKind::Segment {
15729            segment: Segment::Line(line1_data),
15730        } = &line1.kind
15731        else {
15732            panic!("Expected line");
15733        };
15734        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15735        let ObjectKind::Segment {
15736            segment: Segment::Line(line2_data),
15737        } = &line2.kind
15738        else {
15739            panic!("Expected line");
15740        };
15741
15742        // Build constraint before entering sketch mode.
15743        let constraint = Constraint::Coincident(Coincident {
15744            segments: vec![line1_data.end.into(), line2_data.start.into()],
15745        });
15746
15747        // Enter sketch edit mode.
15748        frontend
15749            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15750            .await
15751            .unwrap();
15752        let (src_delta, _scene_delta) = frontend
15753            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15754            .await
15755            .unwrap();
15756        assert!(
15757            src_delta.text.contains("coincident("),
15758            "Expected coincident constraint in source, got: {}",
15759            src_delta.text
15760        );
15761
15762        ctx.close().await;
15763        mock_ctx.close().await;
15764    }
15765
15766    #[tokio::test(flavor = "multi_thread")]
15767    async fn test_extra_newlines_add_line_then_edit_line() {
15768        // Extra newlines after @settings - add a line, then edit it.
15769        let initial_source = "@settings(defaultLengthUnit = mm)
15770
15771sketch001 = sketch(on = XY) {
15772}
15773";
15774
15775        let program = Program::parse(initial_source).unwrap().0.unwrap();
15776        let mut frontend = FrontendState::new();
15777
15778        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15779        let mock_ctx = ExecutorContext::new_mock(None).await;
15780        let version = Version(0);
15781
15782        frontend.hack_set_program(&ctx, program).await.unwrap();
15783        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15784        let sketch_id = sketch_object.id;
15785
15786        // Add a line.
15787        let line_ctor = LineCtor {
15788            start: Point2d {
15789                x: Expr::Number(Number {
15790                    value: 0.0,
15791                    units: NumericSuffix::Mm,
15792                }),
15793                y: Expr::Number(Number {
15794                    value: 0.0,
15795                    units: NumericSuffix::Mm,
15796                }),
15797            },
15798            end: Point2d {
15799                x: Expr::Number(Number {
15800                    value: 10.0,
15801                    units: NumericSuffix::Mm,
15802                }),
15803                y: Expr::Number(Number {
15804                    value: 10.0,
15805                    units: NumericSuffix::Mm,
15806                }),
15807            },
15808            construction: None,
15809        };
15810        let segment = SegmentCtor::Line(line_ctor);
15811        let (src_delta, scene_delta) = frontend
15812            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15813            .await
15814            .unwrap();
15815        assert!(
15816            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15817            "Expected line in source after add, got: {}",
15818            src_delta.text
15819        );
15820        // Line creates start point, end point, and line segment.
15821        let line_id = *scene_delta.new_objects.last().unwrap();
15822
15823        // Edit the line.
15824        let line_ctor = LineCtor {
15825            start: Point2d {
15826                x: Expr::Number(Number {
15827                    value: 1.0,
15828                    units: NumericSuffix::Mm,
15829                }),
15830                y: Expr::Number(Number {
15831                    value: 2.0,
15832                    units: NumericSuffix::Mm,
15833                }),
15834            },
15835            end: Point2d {
15836                x: Expr::Number(Number {
15837                    value: 13.0,
15838                    units: NumericSuffix::Mm,
15839                }),
15840                y: Expr::Number(Number {
15841                    value: 14.0,
15842                    units: NumericSuffix::Mm,
15843                }),
15844            },
15845            construction: None,
15846        };
15847        let segments = vec![ExistingSegmentCtor {
15848            id: line_id,
15849            ctor: SegmentCtor::Line(line_ctor),
15850        }];
15851        let (src_delta, scene_delta) = frontend
15852            .edit_segments(&mock_ctx, version, sketch_id, segments)
15853            .await
15854            .unwrap();
15855        assert!(
15856            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15857            "Expected edited line in source, got: {}",
15858            src_delta.text
15859        );
15860        assert_eq!(scene_delta.new_objects, vec![]);
15861
15862        ctx.close().await;
15863        mock_ctx.close().await;
15864    }
15865
15866    #[test]
15867    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15868        // A non-target sketch block appears before the target so that the
15869        // traversal enters and leaves it before reaching the target. The
15870        // generated name must come from the top-level scope, where foo1 is
15871        // taken, not the sketch's scope, where no foo names are taken. This
15872        // is a regression test: dfs_mut used to visit the sketch block twice,
15873        // pushing its scope twice but popping it once, leaving the sketch
15874        // scope on top of the defined-names stack for the rest of the
15875        // traversal.
15876        let code = "\
15877foo1 = 1
15878sk = sketch() {
15879  p = var 1.5
15880}
158817 + 8
15882";
15883        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15884        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15885            panic!("expected an expression statement");
15886        };
15887        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15888        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15889            &mut ast,
15890            &source_ref,
15891            AstMutateCommand::AddVariableDeclaration {
15892                prefix: "foo".to_owned(),
15893            },
15894        )
15895        .unwrap();
15896        let AstMutateCommandReturn::Name(name) = cmd_return else {
15897            panic!("expected a generated name");
15898        };
15899        assert_eq!(name, "foo2");
15900        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15901            panic!("expected the expression statement to become a variable declaration");
15902        };
15903        assert_eq!(decl.name(), "foo2");
15904    }
15905
15906    /// Get the function body of the variable declaration at `ast.body[index]`.
15907    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15908        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15909            panic!("expected a variable declaration");
15910        };
15911        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15912            panic!("expected a function expression");
15913        };
15914        &func.body
15915    }
15916
15917    /// Get the then-branch block of the if-expression initializing the
15918    /// variable declaration at `ast.body[index]`.
15919    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15920        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15921            panic!("expected a variable declaration");
15922        };
15923        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15924            panic!("expected an if expression");
15925        };
15926        &if_expr.then_val
15927    }
15928
15929    #[test]
15930    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15931        // The generated name must come from the function body's scope, where
15932        // thing1 is taken. Before dfs_mut visited function bodies as program
15933        // nodes, the top-level scope was used instead, generating thing1 and
15934        // colliding with the local.
15935        let code = "\
15936fn build() {
15937  thing1 = 1
15938  10 + 20
15939  return thing1
15940}
15941";
15942        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15943        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15944            panic!("expected an expression statement");
15945        };
15946        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15947        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15948            &mut ast,
15949            &source_ref,
15950            AstMutateCommand::AddVariableDeclaration {
15951                prefix: "thing".to_owned(),
15952            },
15953        )
15954        .unwrap();
15955        let AstMutateCommandReturn::Name(name) = cmd_return else {
15956            panic!("expected a generated name");
15957        };
15958        assert_eq!(name, "thing2");
15959        let body = &function_body_at(&ast, 0).body;
15960        assert_eq!(body.len(), 3);
15961        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15962            panic!("expected the expression statement to become a variable declaration");
15963        };
15964        assert_eq!(decl.name(), "thing2");
15965        // Siblings are untouched.
15966        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15967            panic!("expected a variable declaration");
15968        };
15969        assert_eq!(first.name(), "thing1");
15970        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15971    }
15972
15973    #[test]
15974    fn test_delete_node_in_function_body_preserves_leading_comment() {
15975        // Before the shared body traversal, MutateBodyItem::Delete was
15976        // silently dropped inside function bodies, so this reported success
15977        // without deleting anything.
15978        let code = "\
15979fn build() {
15980  a = 1
15981  // keep me
15982  b = 2
15983  return a
15984}
15985";
15986        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15987        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15988            panic!("expected a variable declaration");
15989        };
15990        assert_eq!(b_decl.name(), "b");
15991        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15992        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15993        let body = &function_body_at(&ast, 0).body;
15994        assert_eq!(body.len(), 2, "expected b to be deleted");
15995        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15996            panic!("expected a variable declaration");
15997        };
15998        assert_eq!(first.name(), "a");
15999        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16000            panic!("expected the return statement to remain");
16001        };
16002        assert!(
16003            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16004            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16005            body[1].get_comments()
16006        );
16007    }
16008
16009    #[test]
16010    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16011        // Locals in the function body are avoided: thing1 is taken, so the
16012        // generated name is thing2. But find_defined_names only sees the
16013        // block's body items, not the function's parameters, so the generated
16014        // name collides with the thing2 parameter. This pins the current
16015        // behavior.
16016        // TODO: Should function parameters be included in the scope used for
16017        // name generation?
16018        let code = "\
16019fn build(thing2) {
16020  thing1 = 1
16021  10 + 20
16022  return thing1 + thing2
16023}
16024";
16025        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16026        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16027            panic!("expected an expression statement");
16028        };
16029        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16030        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16031            &mut ast,
16032            &source_ref,
16033            AstMutateCommand::AddVariableDeclaration {
16034                prefix: "thing".to_owned(),
16035            },
16036        )
16037        .unwrap();
16038        let AstMutateCommandReturn::Name(name) = cmd_return else {
16039            panic!("expected a generated name");
16040        };
16041        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16042    }
16043
16044    #[test]
16045    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16046        let code = "\
16047x = 1
16048y = if x > 0 {
16049  q1 = 1
16050  foo(q1)
16051  q1
16052} else {
16053  2
16054}
16055";
16056        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16057        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16058            panic!("expected an expression statement");
16059        };
16060        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16061        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16062            &mut ast,
16063            &source_ref,
16064            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16065        )
16066        .unwrap();
16067        let AstMutateCommandReturn::Name(name) = cmd_return else {
16068            panic!("expected a generated name");
16069        };
16070        assert_eq!(name, "q2");
16071        let body = &then_block_at(&ast, 1).body;
16072        assert_eq!(body.len(), 3);
16073        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16074            panic!("expected the expression statement to become a variable declaration");
16075        };
16076        assert_eq!(decl.name(), "q2");
16077        // Siblings are untouched.
16078        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16079            panic!("expected a variable declaration");
16080        };
16081        assert_eq!(first.name(), "q1");
16082        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16083    }
16084
16085    #[test]
16086    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16087        // Before the shared body traversal, MutateBodyItem::Delete was
16088        // silently dropped inside if-expression branch blocks.
16089        let code = "\
16090y = if true {
16091  a = 1
16092  // keep me
16093  b = 2
16094  a + b
16095} else {
16096  2
16097}
16098";
16099        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16100        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16101            panic!("expected a variable declaration");
16102        };
16103        assert_eq!(b_decl.name(), "b");
16104        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16105        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16106        let body = &then_block_at(&ast, 0).body;
16107        assert_eq!(body.len(), 2, "expected b to be deleted");
16108        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16109            panic!("expected a variable declaration");
16110        };
16111        assert_eq!(first.name(), "a");
16112        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16113            panic!("expected the tail expression to remain");
16114        };
16115        assert!(
16116            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16117            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16118            body[1].get_comments()
16119        );
16120    }
16121}