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    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965        self.program = program.clone();
1966
1967        // Execute so that the objects are updated and available for the next
1968        // API call.
1969        // This always uses engine execution (not mock) so that things are cached.
1970        // Engine execution now runs freedom analysis automatically.
1971        // Keep existing checkpoints alive here. History may still reference
1972        // older committed sketch states across a direct-edit boundary, and a
1973        // checkpoint restore is a full state replacement anyway. We append a
1974        // fresh baseline checkpoint after the full execution below.
1975        // Clear the freedom cache since IDs might have changed after direct editing
1976        // and we're about to run freedom analysis which will repopulate it.
1977        self.point_freedom_cache.clear();
1978        match ctx.run_with_caching(program).await {
1979            Ok(outcome) => {
1980                let outcome = self.update_state_after_exec(outcome, true);
1981                let checkpoint_id = self
1982                    .create_sketch_checkpoint(outcome.clone())
1983                    .await
1984                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985                Ok(SetProgramOutcome::Success {
1986                    scene_graph: Box::new(self.scene_graph_for_ui()),
1987                    exec_outcome: Box::new(outcome),
1988                    checkpoint_id: Some(checkpoint_id),
1989                })
1990            }
1991            Err(mut err) => {
1992                // Don't return an error just because execution failed. Instead,
1993                // update state as much as possible.
1994                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995                self.update_state_after_exec(outcome, true);
1996                err.scene_graph = Some(self.scene_graph_for_ui());
1997                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998            }
1999        }
2000    }
2001
2002    /// Decorate engine execution such that our state is updated and the scene
2003    /// graph is added to the return.
2004    pub async fn engine_execute(
2005        &mut self,
2006        ctx: &ExecutorContext,
2007        program: Program,
2008    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009        self.program = program.clone();
2010
2011        // Engine execution now runs freedom analysis automatically. Clear the
2012        // freedom cache since IDs might have changed after direct editing, and
2013        // we're about to run freedom analysis which will repopulate it.
2014        self.point_freedom_cache.clear();
2015        match ctx.run_with_caching(program).await {
2016            Ok(outcome) => {
2017                let outcome = self.update_state_after_exec(outcome, true);
2018                Ok(SceneGraphDelta {
2019                    new_graph: self.scene_graph_for_ui(),
2020                    exec_outcome: outcome,
2021                    // We don't know what the new objects are.
2022                    new_objects: Default::default(),
2023                    // We don't know if IDs were invalidated.
2024                    invalidates_ids: Default::default(),
2025                })
2026            }
2027            Err(mut err) => {
2028                // Update state as much as possible, even when there's an error.
2029                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030                self.update_state_after_exec(outcome, true);
2031                err.scene_graph = Some(self.scene_graph_for_ui());
2032                Err(err)
2033            }
2034        }
2035    }
2036
2037    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038        if matches!(err.error, KclError::EngineHangup { .. }) {
2039            // It's not ideal to special-case this, but this error is very
2040            // common during development, and it causes confusing downstream
2041            // errors that have nothing to do with the actual problem.
2042            return Err(err);
2043        }
2044
2045        let KclErrorWithOutputs {
2046            error,
2047            mut non_fatal,
2048            variables,
2049            operations,
2050            artifact_graph,
2051            scene_objects,
2052            source_range_to_object,
2053            var_solutions,
2054            refactor_metadata,
2055            filenames,
2056            source_files,
2057            default_planes,
2058            ..
2059        } = err;
2060
2061        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063        Ok(ExecOutcome {
2064            variables,
2065            #[cfg(test)]
2066            test_program_memory: Default::default(),
2067            filenames,
2068            operations,
2069            artifact_graph,
2070            scene_objects,
2071            source_range_to_object,
2072            var_solutions,
2073            refactor_metadata,
2074            issues: non_fatal,
2075            source_files,
2076            default_planes,
2077        })
2078    }
2079
2080    async fn add_point(
2081        &mut self,
2082        ctx: &ExecutorContext,
2083        sketch: ObjectId,
2084        ctor: PointCtor,
2085    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2086        // Create updated KCL source from args.
2087        let at_ast = to_ast_point2d(&ctor.position)
2088            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2089        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2090            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2091            unlabeled: None,
2092            arguments: vec![ast::LabeledArg {
2093                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2094                arg: at_ast,
2095            }],
2096            digest: None,
2097            non_code_meta: Default::default(),
2098        })));
2099
2100        // Look up existing sketch.
2101        let sketch_id = sketch;
2102        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103            #[cfg(target_arch = "wasm32")]
2104            web_sys::console::error_1(
2105                &format!(
2106                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2107                    self.scene_graph.objects
2108                )
2109                .into(),
2110            );
2111            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2112        })?;
2113        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2114            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2115                "Object is not a sketch, it is {}",
2116                sketch_object.kind.human_friendly_kind_with_article(),
2117            ))));
2118        };
2119        // Add the point to the AST of the sketch block.
2120        let mut new_ast = self.program.ast.clone();
2121        let (sketch_block_ref, _) = self
2122            .mutate_ast(
2123                &mut new_ast,
2124                sketch_id,
2125                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2126            )
2127            .map_err(KclErrorWithOutputs::no_outputs)?;
2128        // Convert to string source to create real source ranges.
2129        let new_source = source_from_ast(&new_ast);
2130        // Parse the new KCL source.
2131        let new_program = parse_frontend_mutation_source(
2132            &new_source,
2133            "Error parsing KCL source after adding point",
2134            "No AST produced after adding point",
2135        )?;
2136
2137        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2138            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2139                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2140            )))
2141        })?;
2142
2143        // Make sure to only set this if there are no errors.
2144        self.program = new_program.clone();
2145
2146        // Truncate after the sketch block for mock execution.
2147        let mut truncated_program = new_program;
2148        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2149            .map_err(KclErrorWithOutputs::no_outputs)?;
2150
2151        // Execute.
2152        let outcome = ctx
2153            .run_mock(
2154                &truncated_program,
2155                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2156            )
2157            .await?;
2158
2159        let new_object_ids = {
2160            let make_err =
2161                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2162            let segment_id = outcome
2163                .source_range_to_object
2164                .get(&point_node_ref.range)
2165                .copied()
2166                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2167            let segment_object = outcome
2168                .scene_objects
2169                .get(segment_id.0)
2170                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2171            let ObjectKind::Segment { segment } = &segment_object.kind else {
2172                return Err(make_err(format!(
2173                    "Object is not a segment, it is {}",
2174                    segment_object.kind.human_friendly_kind_with_article()
2175                )));
2176            };
2177            let Segment::Point(_) = segment else {
2178                return Err(make_err(format!(
2179                    "Segment is not a point, it is {}",
2180                    segment.human_friendly_kind_with_article()
2181                )));
2182            };
2183            vec![segment_id]
2184        };
2185        let src_delta = SourceDelta { text: new_source };
2186        // Uses .no_freedom_analysis() so freedom_analysis: false
2187        let outcome = self.update_state_after_exec(outcome, false);
2188        let scene_graph_delta = SceneGraphDelta {
2189            new_graph: self.scene_graph_for_ui(),
2190            invalidates_ids: false,
2191            new_objects: new_object_ids,
2192            exec_outcome: outcome,
2193        };
2194        Ok((src_delta, scene_graph_delta))
2195    }
2196
2197    async fn add_line(
2198        &mut self,
2199        ctx: &ExecutorContext,
2200        sketch: ObjectId,
2201        ctor: LineCtor,
2202    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2203        // Create updated KCL source from args.
2204        let start_ast = to_ast_point2d(&ctor.start)
2205            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206        let end_ast = to_ast_point2d(&ctor.end)
2207            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208        let mut arguments = vec![
2209            ast::LabeledArg {
2210                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2211                arg: start_ast,
2212            },
2213            ast::LabeledArg {
2214                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2215                arg: end_ast,
2216            },
2217        ];
2218        // Add construction kwarg if construction is Some(true)
2219        if ctor.construction == Some(true) {
2220            arguments.push(ast::LabeledArg {
2221                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2222                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2223                    value: ast::LiteralValue::Bool(true),
2224                    raw: "true".to_string(),
2225                    digest: None,
2226                }))),
2227            });
2228        }
2229        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2230            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2231            unlabeled: None,
2232            arguments,
2233            digest: None,
2234            non_code_meta: Default::default(),
2235        })));
2236
2237        // Look up existing sketch.
2238        let sketch_id = sketch;
2239        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2240            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2241        })?;
2242        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2243            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2244                "Object is not a sketch, it is {}",
2245                sketch_object.kind.human_friendly_kind_with_article(),
2246            ))));
2247        };
2248        // Add the line to the AST of the sketch block.
2249        let mut new_ast = self.program.ast.clone();
2250        let (sketch_block_ref, _) = self
2251            .mutate_ast(
2252                &mut new_ast,
2253                sketch_id,
2254                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2255            )
2256            .map_err(KclErrorWithOutputs::no_outputs)?;
2257        // Convert to string source to create real source ranges.
2258        let new_source = source_from_ast(&new_ast);
2259        // Parse the new KCL source.
2260        let new_program = parse_frontend_mutation_source(
2261            &new_source,
2262            "Error parsing KCL source after adding line",
2263            "No AST produced after adding line",
2264        )?;
2265
2266        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2267            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2269            )))
2270        })?;
2271
2272        // Make sure to only set this if there are no errors.
2273        self.program = new_program.clone();
2274
2275        // Truncate after the sketch block for mock execution.
2276        let mut truncated_program = new_program;
2277        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2278            .map_err(KclErrorWithOutputs::no_outputs)?;
2279
2280        // Execute.
2281        let outcome = ctx
2282            .run_mock(
2283                &truncated_program,
2284                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2285            )
2286            .await?;
2287
2288        let new_object_ids = {
2289            let make_err =
2290                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2291            let segment_id = outcome
2292                .source_range_to_object
2293                .get(&line_node_ref.range)
2294                .copied()
2295                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2296            let segment_object = outcome
2297                .scene_object_by_id(segment_id)
2298                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2299            let ObjectKind::Segment { segment } = &segment_object.kind else {
2300                return Err(make_err(format!(
2301                    "Object is not a segment, it is {}",
2302                    segment_object.kind.human_friendly_kind_with_article()
2303                )));
2304            };
2305            let Segment::Line(line) = segment else {
2306                return Err(make_err(format!(
2307                    "Segment is not a line, it is {}",
2308                    segment.human_friendly_kind_with_article()
2309                )));
2310            };
2311            vec![line.start, line.end, segment_id]
2312        };
2313        let src_delta = SourceDelta { text: new_source };
2314        // Uses .no_freedom_analysis() so freedom_analysis: false
2315        let outcome = self.update_state_after_exec(outcome, false);
2316        let scene_graph_delta = SceneGraphDelta {
2317            new_graph: self.scene_graph_for_ui(),
2318            invalidates_ids: false,
2319            new_objects: new_object_ids,
2320            exec_outcome: outcome,
2321        };
2322        Ok((src_delta, scene_graph_delta))
2323    }
2324
2325    async fn add_arc(
2326        &mut self,
2327        ctx: &ExecutorContext,
2328        sketch: ObjectId,
2329        ctor: ArcCtor,
2330    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2331        // Create updated KCL source from args.
2332        let start_ast = to_ast_point2d(&ctor.start)
2333            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334        let end_ast = to_ast_point2d(&ctor.end)
2335            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336        let center_ast = to_ast_point2d(&ctor.center)
2337            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2338        let mut arguments = vec![
2339            ast::LabeledArg {
2340                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2341                arg: start_ast,
2342            },
2343            ast::LabeledArg {
2344                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2345                arg: end_ast,
2346            },
2347            ast::LabeledArg {
2348                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2349                arg: center_ast,
2350            },
2351        ];
2352        // Add direction kwarg if it's clockwise, since counterclockwise is the
2353        // default.
2354        if ctor.direction == Some(ArcDirection::Cw) {
2355            arguments.push(ast::LabeledArg {
2356                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2357                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2358            });
2359        }
2360        // Add construction kwarg if construction is Some(true)
2361        if ctor.construction == Some(true) {
2362            arguments.push(ast::LabeledArg {
2363                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2364                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2365                    value: ast::LiteralValue::Bool(true),
2366                    raw: "true".to_string(),
2367                    digest: None,
2368                }))),
2369            });
2370        }
2371        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2372            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2373            unlabeled: None,
2374            arguments,
2375            digest: None,
2376            non_code_meta: Default::default(),
2377        })));
2378
2379        // Look up existing sketch.
2380        let sketch_id = sketch;
2381        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2382            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2383        })?;
2384        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2385            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2386                "Object is not a sketch, it is {}",
2387                sketch_object.kind.human_friendly_kind_with_article(),
2388            ))));
2389        };
2390        // Add the arc to the AST of the sketch block.
2391        let mut new_ast = self.program.ast.clone();
2392        let (sketch_block_ref, _) = self
2393            .mutate_ast(
2394                &mut new_ast,
2395                sketch_id,
2396                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2397            )
2398            .map_err(KclErrorWithOutputs::no_outputs)?;
2399        // Convert to string source to create real source ranges.
2400        let new_source = source_from_ast(&new_ast);
2401        // Parse the new KCL source.
2402        let new_program = parse_frontend_mutation_source(
2403            &new_source,
2404            "Error parsing KCL source after adding arc",
2405            "No AST produced after adding arc",
2406        )?;
2407
2408        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2409            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2411            )))
2412        })?;
2413
2414        // Make sure to only set this if there are no errors.
2415        self.program = new_program.clone();
2416
2417        // Truncate after the sketch block for mock execution.
2418        let mut truncated_program = new_program;
2419        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2420            .map_err(KclErrorWithOutputs::no_outputs)?;
2421
2422        // Execute.
2423        let outcome = ctx
2424            .run_mock(
2425                &truncated_program,
2426                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2427            )
2428            .await?;
2429
2430        let new_object_ids = {
2431            let make_err =
2432                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2433            let segment_id = outcome
2434                .source_range_to_object
2435                .get(&arc_node_ref.range)
2436                .copied()
2437                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2438            let segment_object = outcome
2439                .scene_objects
2440                .get(segment_id.0)
2441                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2442            let ObjectKind::Segment { segment } = &segment_object.kind else {
2443                return Err(make_err(format!(
2444                    "Object is not a segment, it is {}",
2445                    segment_object.kind.human_friendly_kind_with_article()
2446                )));
2447            };
2448            let Segment::Arc(arc) = segment else {
2449                return Err(make_err(format!(
2450                    "Segment is not an arc, it is {}",
2451                    segment.human_friendly_kind_with_article()
2452                )));
2453            };
2454            vec![arc.start, arc.end, arc.center, segment_id]
2455        };
2456        let src_delta = SourceDelta { text: new_source };
2457        // Uses .no_freedom_analysis() so freedom_analysis: false
2458        let outcome = self.update_state_after_exec(outcome, false);
2459        let scene_graph_delta = SceneGraphDelta {
2460            new_graph: self.scene_graph_for_ui(),
2461            invalidates_ids: false,
2462            new_objects: new_object_ids,
2463            exec_outcome: outcome,
2464        };
2465        Ok((src_delta, scene_graph_delta))
2466    }
2467
2468    async fn add_circle(
2469        &mut self,
2470        ctx: &ExecutorContext,
2471        sketch: ObjectId,
2472        ctor: CircleCtor,
2473    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2474        // Create updated KCL source from args.
2475        let start_ast = to_ast_point2d(&ctor.start)
2476            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477        let center_ast = to_ast_point2d(&ctor.center)
2478            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2479        let mut arguments = vec![
2480            ast::LabeledArg {
2481                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2482                arg: start_ast,
2483            },
2484            ast::LabeledArg {
2485                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2486                arg: center_ast,
2487            },
2488        ];
2489        // Add construction kwarg if construction is Some(true)
2490        if ctor.construction == Some(true) {
2491            arguments.push(ast::LabeledArg {
2492                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2493                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2494                    value: ast::LiteralValue::Bool(true),
2495                    raw: "true".to_string(),
2496                    digest: None,
2497                }))),
2498            });
2499        }
2500        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2501            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2502            unlabeled: None,
2503            arguments,
2504            digest: None,
2505            non_code_meta: Default::default(),
2506        })));
2507
2508        // Look up existing sketch.
2509        let sketch_id = sketch;
2510        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2511            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2512        })?;
2513        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2514            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2515                "Object is not a sketch, it is {}",
2516                sketch_object.kind.human_friendly_kind_with_article(),
2517            ))));
2518        };
2519        // Add the circle to the AST of the sketch block.
2520        let mut new_ast = self.program.ast.clone();
2521        let (sketch_block_ref, _) = self
2522            .mutate_ast(
2523                &mut new_ast,
2524                sketch_id,
2525                AstMutateCommand::AddSketchBlockVarDecl {
2526                    prefix: CIRCLE_VARIABLE.to_owned(),
2527                    expr: circle_ast,
2528                },
2529            )
2530            .map_err(KclErrorWithOutputs::no_outputs)?;
2531        // Convert to string source to create real source ranges.
2532        let new_source = source_from_ast(&new_ast);
2533        // Parse the new KCL source.
2534        let new_program = parse_frontend_mutation_source(
2535            &new_source,
2536            "Error parsing KCL source after adding circle",
2537            "No AST produced after adding circle",
2538        )?;
2539
2540        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2541            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2542                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2543            )))
2544        })?;
2545
2546        // Make sure to only set this if there are no errors.
2547        self.program = new_program.clone();
2548
2549        // Truncate after the sketch block for mock execution.
2550        let mut truncated_program = new_program;
2551        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2552            .map_err(KclErrorWithOutputs::no_outputs)?;
2553
2554        // Execute.
2555        let outcome = ctx
2556            .run_mock(
2557                &truncated_program,
2558                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2559            )
2560            .await?;
2561
2562        let new_object_ids = {
2563            let make_err =
2564                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2565            let segment_id = outcome
2566                .source_range_to_object
2567                .get(&circle_node_ref.range)
2568                .copied()
2569                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2570            let segment_object = outcome
2571                .scene_objects
2572                .get(segment_id.0)
2573                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2574            let ObjectKind::Segment { segment } = &segment_object.kind else {
2575                return Err(make_err(format!(
2576                    "Object is not a segment, it is {}",
2577                    segment_object.kind.human_friendly_kind_with_article()
2578                )));
2579            };
2580            let Segment::Circle(circle) = segment else {
2581                return Err(make_err(format!(
2582                    "Segment is not a circle, it is {}",
2583                    segment.human_friendly_kind_with_article()
2584                )));
2585            };
2586            vec![circle.start, circle.center, segment_id]
2587        };
2588        let src_delta = SourceDelta { text: new_source };
2589        // Uses .no_freedom_analysis() so freedom_analysis: false
2590        let outcome = self.update_state_after_exec(outcome, false);
2591        let scene_graph_delta = SceneGraphDelta {
2592            new_graph: self.scene_graph_for_ui(),
2593            invalidates_ids: false,
2594            new_objects: new_object_ids,
2595            exec_outcome: outcome,
2596        };
2597        Ok((src_delta, scene_graph_delta))
2598    }
2599
2600    async fn add_control_point_spline(
2601        &mut self,
2602        ctx: &ExecutorContext,
2603        sketch: ObjectId,
2604        ctor: ControlPointSplineCtor,
2605    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2606        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2607            .map_err(KclErrorWithOutputs::no_outputs)?;
2608
2609        let points_ast = to_ast_point2d_array(&ctor.points)
2610            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2611        let mut arguments = vec![ast::LabeledArg {
2612            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2613            arg: points_ast,
2614        }];
2615        if ctor.construction == Some(true) {
2616            arguments.push(ast::LabeledArg {
2617                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2618                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2619                    value: ast::LiteralValue::Bool(true),
2620                    raw: "true".to_string(),
2621                    digest: None,
2622                }))),
2623            });
2624        }
2625        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2626            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2627            unlabeled: None,
2628            arguments,
2629            digest: None,
2630            non_code_meta: Default::default(),
2631        })));
2632
2633        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2634            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2635        })?;
2636        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2637            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2638                "Object is not a sketch, it is {}",
2639                sketch_object.kind.human_friendly_kind_with_article(),
2640            ))));
2641        };
2642
2643        let mut new_ast = new_program.ast.clone();
2644        let (sketch_block_ref, _) = self
2645            .mutate_ast(
2646                &mut new_ast,
2647                sketch,
2648                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2649            )
2650            .map_err(KclErrorWithOutputs::no_outputs)?;
2651        let new_source = source_from_ast(&new_ast);
2652        let new_program = parse_frontend_mutation_source(
2653            &new_source,
2654            "Error parsing KCL source after adding controlPointSpline",
2655            "No AST produced after adding controlPointSpline",
2656        )?;
2657
2658        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2659            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2661            )))
2662        })?;
2663
2664        self.program = new_program.clone();
2665
2666        let mut truncated_program = new_program;
2667        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2668            .map_err(KclErrorWithOutputs::no_outputs)?;
2669
2670        let outcome = ctx
2671            .run_mock(
2672                &truncated_program,
2673                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2674            )
2675            .await?;
2676
2677        let new_object_ids = {
2678            let make_err =
2679                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2680            let segment_id = outcome
2681                .source_range_to_object
2682                .get(&spline_node_ref.range)
2683                .copied()
2684                .ok_or_else(|| {
2685                    make_err(format!(
2686                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2687                    ))
2688                })?;
2689            let segment_object = outcome
2690                .scene_objects
2691                .get(segment_id.0)
2692                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2693            let ObjectKind::Segment { segment } = &segment_object.kind else {
2694                return Err(make_err(format!(
2695                    "Object is not a segment, it is {}",
2696                    segment_object.kind.human_friendly_kind_with_article()
2697                )));
2698            };
2699            let Segment::ControlPointSpline(spline) = segment else {
2700                return Err(make_err(format!(
2701                    "Segment is not a control point spline, it is {}",
2702                    segment.human_friendly_kind_with_article()
2703                )));
2704            };
2705
2706            let mut ids = outcome
2707                .scene_objects
2708                .iter()
2709                .filter_map(|obj| match &obj.kind {
2710                    ObjectKind::Segment {
2711                        segment: Segment::Line(line),
2712                    } if line.owner == Some(segment_id) => Some(obj.id),
2713                    _ => None,
2714                })
2715                .collect::<Vec<_>>();
2716            ids.extend(spline.controls.clone());
2717            ids.push(segment_id);
2718            ids
2719        };
2720        let src_delta = SourceDelta { text: new_source };
2721        let outcome = self.update_state_after_exec(outcome, false);
2722        let scene_graph_delta = SceneGraphDelta {
2723            new_graph: self.scene_graph_for_ui(),
2724            invalidates_ids: false,
2725            new_objects: new_object_ids,
2726            exec_outcome: outcome,
2727        };
2728        Ok((src_delta, scene_graph_delta))
2729    }
2730
2731    fn edit_point(
2732        &mut self,
2733        new_ast: &mut ast::Node<ast::Program>,
2734        sketch: ObjectId,
2735        point: ObjectId,
2736        ctor: PointCtor,
2737    ) -> Result<(), KclError> {
2738        // Create updated KCL source from args.
2739        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2740
2741        // Look up existing sketch.
2742        let sketch_id = sketch;
2743        let sketch_object = self
2744            .scene_graph
2745            .objects
2746            .get(sketch_id.0)
2747            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2748        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2749            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2750        };
2751        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2752            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2753        })?;
2754        // Look up existing point.
2755        let point_id = point;
2756        let point_object = self
2757            .scene_graph
2758            .objects
2759            .get(point_id.0)
2760            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2761        let ObjectKind::Segment {
2762            segment: Segment::Point(point),
2763        } = &point_object.kind
2764        else {
2765            return Err(KclError::refactor(format!(
2766                "Object is not a point segment: {point_object:?}"
2767            )));
2768        };
2769
2770        // If the point is part of a line or arc, edit the line/arc instead.
2771        if let Some(owner_id) = point.owner {
2772            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2773                KclError::refactor(format!(
2774                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2775                ))
2776            })?;
2777            let ObjectKind::Segment { segment } = &owner_object.kind else {
2778                return Err(KclError::refactor(format!(
2779                    "Internal: Owner of point is not a segment, but found {}",
2780                    owner_object.kind.human_friendly_kind_with_article()
2781                )));
2782            };
2783
2784            // Handle Line owner
2785            if let Segment::Line(line) = segment {
2786                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2787                    return Err(KclError::refactor(format!(
2788                        "Internal: Owner of point does not have line ctor, but found {}",
2789                        line.ctor.human_friendly_kind_with_article()
2790                    )));
2791                };
2792                let mut line_ctor = line_ctor.clone();
2793                // Which end of the line is this point?
2794                if line.start == point_id {
2795                    line_ctor.start = ctor.position;
2796                } else if line.end == point_id {
2797                    line_ctor.end = ctor.position;
2798                } else {
2799                    return Err(KclError::refactor(format!(
2800                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2801                    )));
2802                }
2803                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2804            }
2805
2806            // Handle Arc owner
2807            if let Segment::Arc(arc) = segment {
2808                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2809                    return Err(KclError::refactor(format!(
2810                        "Internal: Owner of point does not have arc ctor, but found {}",
2811                        arc.ctor.human_friendly_kind_with_article()
2812                    )));
2813                };
2814                let mut arc_ctor = arc_ctor.clone();
2815                // Which point of the arc is this? (center, start, or end)
2816                if arc.center == point_id {
2817                    arc_ctor.center = ctor.position;
2818                } else if arc.start == point_id {
2819                    arc_ctor.start = ctor.position;
2820                } else if arc.end == point_id {
2821                    arc_ctor.end = ctor.position;
2822                } else {
2823                    return Err(KclError::refactor(format!(
2824                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2825                    )));
2826                }
2827                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2828            }
2829
2830            // Handle Circle owner
2831            if let Segment::Circle(circle) = segment {
2832                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2833                    return Err(KclError::refactor(format!(
2834                        "Internal: Owner of point does not have circle ctor, but found {}",
2835                        circle.ctor.human_friendly_kind_with_article()
2836                    )));
2837                };
2838                let mut circle_ctor = circle_ctor.clone();
2839                if circle.center == point_id {
2840                    circle_ctor.center = ctor.position;
2841                } else if circle.start == point_id {
2842                    circle_ctor.start = ctor.position;
2843                } else {
2844                    return Err(KclError::refactor(format!(
2845                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2846                    )));
2847                }
2848                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2849            }
2850
2851            if let Segment::ControlPointSpline(spline) = segment {
2852                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2853                    return Err(KclError::refactor(format!(
2854                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2855                        spline.ctor.human_friendly_kind_with_article()
2856                    )));
2857                };
2858                let mut spline_ctor = spline_ctor.clone();
2859                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2860                    return Err(KclError::refactor(format!(
2861                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2862                    )));
2863                };
2864                spline_ctor.points[control_index] = ctor.position;
2865                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2866            }
2867
2868            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2869            // (fall through to the point editing logic below)
2870        }
2871
2872        // Modify the point AST.
2873        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2874        Ok(())
2875    }
2876
2877    fn edit_line(
2878        &mut self,
2879        new_ast: &mut ast::Node<ast::Program>,
2880        sketch: ObjectId,
2881        line: ObjectId,
2882        ctor: LineCtor,
2883    ) -> Result<(), KclError> {
2884        // Create updated KCL source from args.
2885        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2886        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2887
2888        // Look up existing sketch.
2889        let sketch_id = sketch;
2890        let sketch_object = self
2891            .scene_graph
2892            .objects
2893            .get(sketch_id.0)
2894            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897        };
2898        sketch
2899            .segments
2900            .iter()
2901            .find(|o| **o == line)
2902            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2903        // Look up existing line.
2904        let line_id = line;
2905        let line_object = self
2906            .scene_graph
2907            .objects
2908            .get(line_id.0)
2909            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2910        let ObjectKind::Segment { .. } = &line_object.kind else {
2911            let kind = line_object.kind.human_friendly_kind_with_article();
2912            return Err(KclError::refactor(format!(
2913                "This constraint only works on Segments, but you selected {kind}"
2914            )));
2915        };
2916
2917        // Modify the line AST.
2918        self.mutate_ast(
2919            new_ast,
2920            line_id,
2921            AstMutateCommand::EditLine {
2922                start: new_start_ast,
2923                end: new_end_ast,
2924                construction: ctor.construction,
2925            },
2926        )?;
2927        Ok(())
2928    }
2929
2930    fn edit_arc(
2931        &mut self,
2932        new_ast: &mut ast::Node<ast::Program>,
2933        sketch: ObjectId,
2934        arc: ObjectId,
2935        ctor: ArcCtor,
2936    ) -> Result<(), KclError> {
2937        // Create updated KCL source from args.
2938        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2939        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2940        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942        // Look up existing sketch.
2943        let sketch_id = sketch;
2944        let sketch_object = self
2945            .scene_graph
2946            .objects
2947            .get(sketch_id.0)
2948            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951        };
2952        sketch
2953            .segments
2954            .iter()
2955            .find(|o| **o == arc)
2956            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2957        // Look up existing arc.
2958        let arc_id = arc;
2959        let arc_object = self
2960            .scene_graph
2961            .objects
2962            .get(arc_id.0)
2963            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2964        let ObjectKind::Segment { .. } = &arc_object.kind else {
2965            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2966        };
2967
2968        // Modify the arc AST.
2969        self.mutate_ast(
2970            new_ast,
2971            arc_id,
2972            AstMutateCommand::EditArc {
2973                start: new_start_ast,
2974                end: new_end_ast,
2975                center: new_center_ast,
2976                direction: ctor.direction,
2977                construction: ctor.construction,
2978            },
2979        )?;
2980        Ok(())
2981    }
2982
2983    fn edit_circle(
2984        &mut self,
2985        new_ast: &mut ast::Node<ast::Program>,
2986        sketch: ObjectId,
2987        circle: ObjectId,
2988        ctor: CircleCtor,
2989    ) -> Result<(), KclError> {
2990        // Create updated KCL source from args.
2991        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2992        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2993
2994        // Look up existing sketch.
2995        let sketch_id = sketch;
2996        let sketch_object = self
2997            .scene_graph
2998            .objects
2999            .get(sketch_id.0)
3000            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3001        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3002            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3003        };
3004        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3005            KclError::refactor(format!(
3006                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3007            ))
3008        })?;
3009        // Look up existing circle.
3010        let circle_id = circle;
3011        let circle_object = self
3012            .scene_graph
3013            .objects
3014            .get(circle_id.0)
3015            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3016        let ObjectKind::Segment { .. } = &circle_object.kind else {
3017            return Err(KclError::refactor(format!(
3018                "Object is not a segment: {circle_object:?}"
3019            )));
3020        };
3021
3022        // Modify the circle AST.
3023        self.mutate_ast(
3024            new_ast,
3025            circle_id,
3026            AstMutateCommand::EditCircle {
3027                start: new_start_ast,
3028                center: new_center_ast,
3029                construction: ctor.construction,
3030            },
3031        )?;
3032        Ok(())
3033    }
3034
3035    fn edit_control_point_spline(
3036        &mut self,
3037        new_ast: &mut ast::Node<ast::Program>,
3038        sketch: ObjectId,
3039        spline: ObjectId,
3040        ctor: ControlPointSplineCtor,
3041    ) -> Result<(), KclError> {
3042        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3043
3044        let sketch_object = self
3045            .scene_graph
3046            .objects
3047            .get(sketch.0)
3048            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3049        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3050            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3051        };
3052        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3053            KclError::refactor(format!(
3054                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3055            ))
3056        })?;
3057
3058        let spline_object =
3059            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3060                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3061            })?;
3062        let ObjectKind::Segment { .. } = &spline_object.kind else {
3063            return Err(KclError::refactor(format!(
3064                "Object is not a segment: {spline_object:?}"
3065            )));
3066        };
3067
3068        self.mutate_ast(
3069            new_ast,
3070            spline,
3071            AstMutateCommand::EditControlPointSpline {
3072                points: points_ast,
3073                construction: ctor.construction,
3074            },
3075        )?;
3076        Ok(())
3077    }
3078
3079    fn delete_segment(
3080        &mut self,
3081        new_ast: &mut ast::Node<ast::Program>,
3082        sketch: ObjectId,
3083        segment_id: ObjectId,
3084    ) -> Result<(), KclError> {
3085        // Look up existing sketch.
3086        let sketch_id = sketch;
3087        let sketch_object = self
3088            .scene_graph
3089            .objects
3090            .get(sketch_id.0)
3091            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3092        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3093            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3094        };
3095        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3096            KclError::refactor(format!(
3097                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3098            ))
3099        })?;
3100        // Look up existing segment.
3101        let segment_object =
3102            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3103                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3104            })?;
3105        let ObjectKind::Segment { .. } = &segment_object.kind else {
3106            return Err(KclError::refactor(format!(
3107                "Object is not a segment, it is {}",
3108                segment_object.kind.human_friendly_kind_with_article()
3109            )));
3110        };
3111
3112        // Modify the AST to remove the segment.
3113        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3114        Ok(())
3115    }
3116
3117    fn delete_constraint(
3118        &mut self,
3119        new_ast: &mut ast::Node<ast::Program>,
3120        sketch: ObjectId,
3121        constraint_id: ObjectId,
3122    ) -> Result<(), KclError> {
3123        // Look up existing sketch.
3124        let sketch_id = sketch;
3125        let sketch_object = self
3126            .scene_graph
3127            .objects
3128            .get(sketch_id.0)
3129            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3130        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3131            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3132        };
3133        sketch
3134            .constraints
3135            .iter()
3136            .find(|o| **o == constraint_id)
3137            .ok_or_else(|| {
3138                KclError::refactor(format!(
3139                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3140                ))
3141            })?;
3142        // Look up existing constraint.
3143        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3144            KclError::refactor(format!(
3145                "Constraint not found in scene graph: constraint={constraint_id:?}"
3146            ))
3147        })?;
3148        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3149            return Err(KclError::refactor(format!(
3150                "Object is not a constraint, it is {}",
3151                constraint_object.kind.human_friendly_kind_with_article()
3152            )));
3153        };
3154
3155        // Modify the AST to remove the constraint.
3156        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3157        Ok(())
3158    }
3159
3160    fn edit_coincident_constraint(
3161        &mut self,
3162        new_ast: &mut ast::Node<ast::Program>,
3163        constraint_id: ObjectId,
3164        segments: Vec<ConstraintSegment>,
3165    ) -> Result<(), KclError> {
3166        if segments.len() < 2 {
3167            return Err(KclError::refactor(format!(
3168                "Coincident constraint must have at least 2 inputs, got {}",
3169                segments.len()
3170            )));
3171        }
3172
3173        let segment_asts = segments
3174            .iter()
3175            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3176            .collect::<Result<Vec<_>, _>>()?;
3177
3178        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3179            elements: segment_asts,
3180            digest: None,
3181            non_code_meta: Default::default(),
3182        })));
3183
3184        self.mutate_ast(
3185            new_ast,
3186            constraint_id,
3187            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3188        )?;
3189        Ok(())
3190    }
3191
3192    fn edit_horizontal_points_constraint(
3193        &mut self,
3194        new_ast: &mut ast::Node<ast::Program>,
3195        constraint_id: ObjectId,
3196        points: Vec<ConstraintSegment>,
3197    ) -> Result<(), KclError> {
3198        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3199    }
3200
3201    fn edit_vertical_points_constraint(
3202        &mut self,
3203        new_ast: &mut ast::Node<ast::Program>,
3204        constraint_id: ObjectId,
3205        points: Vec<ConstraintSegment>,
3206    ) -> Result<(), KclError> {
3207        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3208    }
3209
3210    fn edit_axis_points_constraint(
3211        &mut self,
3212        new_ast: &mut ast::Node<ast::Program>,
3213        constraint_id: ObjectId,
3214        points: Vec<ConstraintSegment>,
3215        constraint_name: &str,
3216    ) -> Result<(), KclError> {
3217        if points.len() < 2 {
3218            return Err(KclError::refactor(format!(
3219                "{constraint_name} points constraint must have at least 2 points, got {}",
3220                points.len()
3221            )));
3222        }
3223
3224        let point_asts = points
3225            .iter()
3226            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3227            .collect::<Result<Vec<_>, _>>()?;
3228
3229        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3230            elements: point_asts,
3231            digest: None,
3232            non_code_meta: Default::default(),
3233        })));
3234
3235        self.mutate_ast(
3236            new_ast,
3237            constraint_id,
3238            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3239        )?;
3240        Ok(())
3241    }
3242
3243    /// updates the equalLength constraint with the given lines
3244    fn edit_equal_length_constraint(
3245        &mut self,
3246        new_ast: &mut ast::Node<ast::Program>,
3247        constraint_id: ObjectId,
3248        lines: Vec<ObjectId>,
3249    ) -> Result<(), KclError> {
3250        if lines.len() < 2 {
3251            return Err(KclError::refactor(format!(
3252                "Lines equal length constraint must have at least 2 lines, got {}",
3253                lines.len()
3254            )));
3255        }
3256
3257        let line_asts = lines
3258            .iter()
3259            .map(|line_id| {
3260                let line_object = self
3261                    .scene_graph
3262                    .objects
3263                    .get(line_id.0)
3264                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3265                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3266                    let kind = line_object.kind.human_friendly_kind_with_article();
3267                    return Err(KclError::refactor(format!(
3268                        "This constraint only works on Segments, but you selected {kind}"
3269                    )));
3270                };
3271                let Segment::Line(_) = line_segment else {
3272                    let kind = line_segment.human_friendly_kind_with_article();
3273                    return Err(KclError::refactor(format!(
3274                        "Only lines can be made equal length, but you selected {kind}"
3275                    )));
3276                };
3277
3278                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3279            })
3280            .collect::<Result<Vec<_>, _>>()?;
3281
3282        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3283            elements: line_asts,
3284            digest: None,
3285            non_code_meta: Default::default(),
3286        })));
3287
3288        self.mutate_ast(
3289            new_ast,
3290            constraint_id,
3291            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3292        )?;
3293        Ok(())
3294    }
3295
3296    /// Updates the parallel constraint with the given lines.
3297    fn edit_parallel_constraint(
3298        &mut self,
3299        new_ast: &mut ast::Node<ast::Program>,
3300        constraint_id: ObjectId,
3301        lines: Vec<ObjectId>,
3302    ) -> Result<(), KclError> {
3303        if lines.len() < 2 {
3304            return Err(KclError::refactor(format!(
3305                "Parallel constraint must have at least 2 lines, got {}",
3306                lines.len()
3307            )));
3308        }
3309
3310        let line_asts = lines
3311            .iter()
3312            .map(|line_id| {
3313                let line_object = self
3314                    .scene_graph
3315                    .objects
3316                    .get(line_id.0)
3317                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3318                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3319                    let kind = line_object.kind.human_friendly_kind_with_article();
3320                    return Err(KclError::refactor(format!(
3321                        "This constraint only works on Segments, but you selected {kind}"
3322                    )));
3323                };
3324                let Segment::Line(_) = line_segment else {
3325                    let kind = line_segment.human_friendly_kind_with_article();
3326                    return Err(KclError::refactor(format!(
3327                        "Only lines can be made parallel, but you selected {kind}"
3328                    )));
3329                };
3330
3331                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3332            })
3333            .collect::<Result<Vec<_>, _>>()?;
3334
3335        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3336            elements: line_asts,
3337            digest: None,
3338            non_code_meta: Default::default(),
3339        })));
3340
3341        self.mutate_ast(
3342            new_ast,
3343            constraint_id,
3344            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3345        )?;
3346        Ok(())
3347    }
3348
3349    /// Updates the equalRadius constraint with the given segments.
3350    fn edit_equal_radius_constraint(
3351        &mut self,
3352        new_ast: &mut ast::Node<ast::Program>,
3353        constraint_id: ObjectId,
3354        input: Vec<ObjectId>,
3355    ) -> Result<(), KclError> {
3356        if input.len() < 2 {
3357            return Err(KclError::refactor(format!(
3358                "equalRadius constraint must have at least 2 segments, got {}",
3359                input.len()
3360            )));
3361        }
3362
3363        let input_asts = input
3364            .iter()
3365            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3366            .collect::<Result<Vec<_>, _>>()?;
3367
3368        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3369            elements: input_asts,
3370            digest: None,
3371            non_code_meta: Default::default(),
3372        })));
3373
3374        self.mutate_ast(
3375            new_ast,
3376            constraint_id,
3377            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3378        )?;
3379        Ok(())
3380    }
3381
3382    async fn execute_after_edit(
3383        &mut self,
3384        ctx: &ExecutorContext,
3385        sketch: ObjectId,
3386        sketch_block_ref: AstNodeRef,
3387        new_ast: &mut ast::Node<ast::Program>,
3388        options: ExecuteAfterEditOptions,
3389    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3390        let ExecuteAfterEditOptions {
3391            segment_ids_edited,
3392            edit_kind,
3393            commit_solved_initial_guesses,
3394        } = options;
3395
3396        // Convert to string source to create real source ranges.
3397        let new_source = source_from_ast(new_ast);
3398        // Parse the new KCL source.
3399        let new_program = parse_frontend_mutation_source(
3400            &new_source,
3401            "Error parsing KCL source after editing",
3402            "No AST produced after editing",
3403        )?;
3404
3405        // Truncate after the sketch block for mock execution.
3406        let is_delete = edit_kind.is_delete();
3407        let truncated_program = {
3408            let mut truncated_program = new_program.clone();
3409            only_sketch_block(
3410                &mut truncated_program.ast,
3411                &sketch_block_ref,
3412                edit_kind.to_change_kind(),
3413            )
3414            .map_err(KclErrorWithOutputs::no_outputs)?;
3415            truncated_program
3416        };
3417
3418        // Execute.
3419        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3420        let mock_config = MockConfig {
3421            sketch_block_id: Some(sketch),
3422            freedom_analysis: is_delete,
3423            segment_ids_edited: segment_ids_edited.clone(),
3424            drag_anchors,
3425            ..Default::default()
3426        };
3427        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3428
3429        // Only now, after execution has succeeded, update self.program.
3430        self.program = new_program;
3431
3432        // Uses freedom_analysis: is_delete
3433        let outcome = self.update_state_after_exec(outcome, is_delete);
3434
3435        let src_delta = if commit_solved_initial_guesses {
3436            self.commit_var_solutions_to_program(&outcome, "editing")?
3437        } else {
3438            SourceDelta { text: new_source }
3439        };
3440        let scene_graph_delta = SceneGraphDelta {
3441            new_graph: self.scene_graph_for_ui(),
3442            invalidates_ids: is_delete,
3443            new_objects: Vec::new(),
3444            exec_outcome: outcome,
3445        };
3446        Ok((src_delta, scene_graph_delta))
3447    }
3448
3449    async fn execute_after_delete_sketch(
3450        &mut self,
3451        ctx: &ExecutorContext,
3452        new_ast: &mut ast::Node<ast::Program>,
3453    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3454        // Convert to string source to create real source ranges.
3455        let new_source = source_from_ast(new_ast);
3456        // Parse the new KCL source.
3457        let new_program = parse_frontend_mutation_source(
3458            &new_source,
3459            "Error parsing KCL source after editing",
3460            "No AST produced after editing",
3461        )?;
3462
3463        // Make sure to only set this if there are no errors.
3464        self.program = new_program.clone();
3465
3466        // We deleted the entire sketch block. It doesn't make sense to truncate
3467        // and execute only the sketch block. We execute the whole program with
3468        // a real engine.
3469
3470        // Execute.
3471        let outcome = ctx.run_with_caching(new_program).await?;
3472        let freedom_analysis_ran = true;
3473
3474        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3475
3476        let src_delta = SourceDelta { text: new_source };
3477        let scene_graph_delta = SceneGraphDelta {
3478            new_graph: self.scene_graph_for_ui(),
3479            invalidates_ids: true,
3480            new_objects: Vec::new(),
3481            exec_outcome: outcome,
3482        };
3483        Ok((src_delta, scene_graph_delta))
3484    }
3485
3486    /// Map a point object id into an AST reference expression for use in
3487    /// constraints. If the point is owned by a segment (line or arc), we
3488    /// reference the appropriate property on that segment (e.g. `line1.start`,
3489    /// `arc1.center`). Otherwise we reference the point directly.
3490    fn point_id_to_ast_reference(
3491        &self,
3492        point_id: ObjectId,
3493        new_ast: &mut ast::Node<ast::Program>,
3494    ) -> Result<ast::Expr, KclError> {
3495        let point_object = self
3496            .scene_graph
3497            .objects
3498            .get(point_id.0)
3499            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3500        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3501            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3502        };
3503        let Segment::Point(point) = point_segment else {
3504            return Err(KclError::refactor(format!(
3505                "Only points are currently supported: {point_object:?}"
3506            )));
3507        };
3508
3509        if let Some(owner_id) = point.owner {
3510            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3511                KclError::refactor(format!(
3512                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3513                ))
3514            })?;
3515            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3516                return Err(KclError::refactor(format!(
3517                    "Owner of point is not a segment, but found {}",
3518                    owner_object.kind.human_friendly_kind_with_article()
3519                )));
3520            };
3521
3522            match owner_segment {
3523                Segment::Line(line) => {
3524                    let property = if line.start == point_id {
3525                        LINE_PROPERTY_START
3526                    } else if line.end == point_id {
3527                        LINE_PROPERTY_END
3528                    } else {
3529                        return Err(KclError::refactor(format!(
3530                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3531                        )));
3532                    };
3533                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3534                }
3535                Segment::Arc(arc) => {
3536                    let property = if arc.start == point_id {
3537                        ARC_PROPERTY_START
3538                    } else if arc.end == point_id {
3539                        ARC_PROPERTY_END
3540                    } else if arc.center == point_id {
3541                        ARC_PROPERTY_CENTER
3542                    } else {
3543                        return Err(KclError::refactor(format!(
3544                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3545                        )));
3546                    };
3547                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3548                }
3549                Segment::Circle(circle) => {
3550                    let property = if circle.start == point_id {
3551                        CIRCLE_PROPERTY_START
3552                    } else if circle.center == point_id {
3553                        CIRCLE_PROPERTY_CENTER
3554                    } else {
3555                        return Err(KclError::refactor(format!(
3556                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3557                        )));
3558                    };
3559                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3560                }
3561                Segment::ControlPointSpline(spline) => {
3562                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3563                        return Err(KclError::refactor(format!(
3564                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3565                        )));
3566                    };
3567                    let owner_expr =
3568                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3569                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3570                    Ok(create_index_expression(controls_expr, index))
3571                }
3572                _ => Err(KclError::refactor(format!(
3573                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3574                ))),
3575            }
3576        } else {
3577            // Standalone point.
3578            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3579        }
3580    }
3581
3582    fn line_id_to_ast_reference(
3583        &self,
3584        line_id: ObjectId,
3585        new_ast: &mut ast::Node<ast::Program>,
3586    ) -> Result<ast::Expr, KclError> {
3587        let line_object = self
3588            .scene_graph
3589            .objects
3590            .get(line_id.0)
3591            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3592        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3593            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3594        };
3595        let Segment::Line(line) = line_segment else {
3596            return Err(KclError::refactor(format!(
3597                "Only lines are currently supported: {line_object:?}"
3598            )));
3599        };
3600
3601        if let Some(owner_id) = line.owner {
3602            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3603                KclError::refactor(format!(
3604                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3605                ))
3606            })?;
3607            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3608                return Err(KclError::refactor(format!(
3609                    "Owner of line is not a segment, but found {}",
3610                    owner_object.kind.human_friendly_kind_with_article()
3611                )));
3612            };
3613
3614            match owner_segment {
3615                Segment::ControlPointSpline(spline) => {
3616                    let Some(index) = spline
3617                        .controls
3618                        .windows(2)
3619                        .position(|window| window[0] == line.start && window[1] == line.end)
3620                    else {
3621                        return Err(KclError::refactor(format!(
3622                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3623                        )));
3624                    };
3625                    let owner_expr =
3626                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3627                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3628                    Ok(create_index_expression(edges_expr, index))
3629                }
3630                _ => Err(KclError::refactor(format!(
3631                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3632                ))),
3633            }
3634        } else {
3635            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3636        }
3637    }
3638
3639    fn coincident_segment_to_ast(
3640        &self,
3641        segment: &ConstraintSegment,
3642        new_ast: &mut ast::Node<ast::Program>,
3643    ) -> Result<ast::Expr, KclError> {
3644        match segment {
3645            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3646            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3647        }
3648    }
3649
3650    fn segment_id_to_constraint_ast_reference(
3651        &self,
3652        segment_id: ObjectId,
3653        new_ast: &mut ast::Node<ast::Program>,
3654    ) -> Result<ast::Expr, KclError> {
3655        let segment_object = self
3656            .scene_graph
3657            .objects
3658            .get(segment_id.0)
3659            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3660        let ObjectKind::Segment { segment } = &segment_object.kind else {
3661            return Err(KclError::refactor(format!(
3662                "Object is not a segment, it is {}",
3663                segment_object.kind.human_friendly_kind_with_article()
3664            )));
3665        };
3666
3667        match segment {
3668            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3669            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3670            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3671            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3672            Segment::ControlPointSpline(_) => {
3673                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3674            }
3675        }
3676    }
3677
3678    fn axis_constraint_segment_to_ast(
3679        &self,
3680        segment: &ConstraintSegment,
3681        new_ast: &mut ast::Node<ast::Program>,
3682    ) -> Result<ast::Expr, KclError> {
3683        match segment {
3684            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3685            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3686        }
3687    }
3688
3689    async fn add_coincident(
3690        &mut self,
3691        sketch: ObjectId,
3692        coincident: Coincident,
3693        new_ast: &mut ast::Node<ast::Program>,
3694    ) -> Result<AstNodeRef, KclError> {
3695        let sketch_id = sketch;
3696        for segment in &coincident.segments {
3697            let ConstraintSegment::Segment(segment_id) = segment else {
3698                continue;
3699            };
3700            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3701                continue;
3702            };
3703            if matches!(
3704                segment_object.kind,
3705                ObjectKind::Segment {
3706                    segment: Segment::ControlPointSpline(_)
3707                }
3708            ) {
3709                return Err(KclError::refactor(
3710                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3711                        .to_owned(),
3712                ));
3713            }
3714        }
3715        let segment_asts = coincident
3716            .segments
3717            .iter()
3718            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3719            .collect::<Result<Vec<_>, _>>()?;
3720        if segment_asts.len() < 2 {
3721            return Err(KclError::refactor(format!(
3722                "Coincident constraint must have at least 2 inputs, got {}",
3723                segment_asts.len()
3724            )));
3725        }
3726
3727        // Create the coincident() call using shared helper.
3728        let coincident_ast = create_coincident_ast(segment_asts);
3729
3730        // Add the line to the AST of the sketch block.
3731        let (sketch_block_ref, _) = self.mutate_ast(
3732            new_ast,
3733            sketch_id,
3734            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3735        )?;
3736        Ok(sketch_block_ref)
3737    }
3738
3739    async fn add_distance(
3740        &mut self,
3741        sketch: ObjectId,
3742        distance: Distance,
3743        new_ast: &mut ast::Node<ast::Program>,
3744    ) -> Result<AstNodeRef, KclError> {
3745        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3746    }
3747
3748    fn distance_constraint_ast_parts(
3749        &self,
3750        function_name: &str,
3751        distance: &Distance,
3752        new_ast: &mut ast::Node<ast::Program>,
3753    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3754        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3755            [pt0, pt1] => [
3756                self.coincident_segment_to_ast(pt0, new_ast)?,
3757                self.coincident_segment_to_ast(pt1, new_ast)?,
3758            ],
3759            _ => {
3760                return Err(KclError::refactor(format!(
3761                    "Distance constraint must have exactly 2 segments, got {}",
3762                    distance.segments.len()
3763                )));
3764            }
3765        };
3766
3767        let arguments = match &distance.label_position {
3768            Some(label_position) => vec![ast::LabeledArg {
3769                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3770                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3771            }],
3772            None => Default::default(),
3773        };
3774
3775        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3776            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3777            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3778                ast::ArrayExpression {
3779                    elements: vec![segment0_ast, segment1_ast],
3780                    digest: None,
3781                    non_code_meta: Default::default(),
3782                },
3783            )))),
3784            arguments,
3785            digest: None,
3786            non_code_meta: Default::default(),
3787        })));
3788        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3789            value: ast::LiteralValue::Number {
3790                value: distance.distance.value,
3791                suffix: distance.distance.units,
3792            },
3793            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3794                KclError::refactor(format!(
3795                    "Could not format numeric suffix: {:?}",
3796                    distance.distance.units
3797                ))
3798            })?,
3799            digest: None,
3800        })));
3801
3802        Ok((call, value))
3803    }
3804
3805    fn add_distance_constraint(
3806        &mut self,
3807        sketch: ObjectId,
3808        function_name: &str,
3809        distance: Distance,
3810        new_ast: &mut ast::Node<ast::Program>,
3811    ) -> Result<AstNodeRef, KclError> {
3812        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3813        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3814            left: call,
3815            operator: ast::BinaryOperator::Eq,
3816            right: value,
3817            digest: None,
3818        })));
3819
3820        let (sketch_block_ref, _) = self.mutate_ast(
3821            new_ast,
3822            sketch,
3823            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3824        )?;
3825        Ok(sketch_block_ref)
3826    }
3827
3828    async fn add_angle(
3829        &mut self,
3830        sketch: ObjectId,
3831        angle: Angle,
3832        new_ast: &mut ast::Node<ast::Program>,
3833    ) -> Result<AstNodeRef, KclError> {
3834        let sketch_id = sketch;
3835        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3836        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3837            left: angle_call_ast,
3838            operator: ast::BinaryOperator::Eq,
3839            right: angle_value_ast,
3840            digest: None,
3841        })));
3842
3843        // Add the line to the AST of the sketch block.
3844        let (sketch_block_ref, _) = self.mutate_ast(
3845            new_ast,
3846            sketch_id,
3847            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3848        )?;
3849        Ok(sketch_block_ref)
3850    }
3851
3852    fn angle_constraint_ast_parts(
3853        &self,
3854        angle: &Angle,
3855        new_ast: &mut ast::Node<ast::Program>,
3856    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3857        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3858            return Err(KclError::refactor(format!(
3859                "Angle constraint must have exactly 2 lines, got {}",
3860                angle.lines.len()
3861            )));
3862        };
3863
3864        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3865        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3866        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3867            elements: vec![l0_ast, l1_ast],
3868            digest: None,
3869            non_code_meta: Default::default(),
3870        })));
3871
3872        if angle.inverse == Some(true) && angle.sector.is_none() {
3873            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3874        }
3875
3876        let uses_angle_dimension = angle.sector.is_some();
3877        let mut arguments = if uses_angle_dimension {
3878            vec![ast::LabeledArg {
3879                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3880                arg: lines_ast.clone(),
3881            }]
3882        } else {
3883            Default::default()
3884        };
3885
3886        if let Some(sector) = angle.sector {
3887            arguments.push(ast::LabeledArg {
3888                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3889                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3890                    value: ast::LiteralValue::Number {
3891                        value: f64::from(sector),
3892                        suffix: NumericSuffix::None,
3893                    },
3894                    raw: sector.to_string(),
3895                    digest: None,
3896                }))),
3897            });
3898        }
3899
3900        if angle.inverse == Some(true) {
3901            arguments.push(ast::LabeledArg {
3902                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3903                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3904                    value: ast::LiteralValue::Bool(true),
3905                    raw: true.to_string(),
3906                    digest: None,
3907                }))),
3908            });
3909        }
3910
3911        if let Some(label_position) = &angle.label_position {
3912            arguments.push(ast::LabeledArg {
3913                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3914                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3915            });
3916        }
3917
3918        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3919            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3920                ANGLE_DIMENSION_FN
3921            } else {
3922                ANGLE_FN
3923            })),
3924            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3925            arguments,
3926            digest: None,
3927            non_code_meta: Default::default(),
3928        })));
3929        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3930            value: ast::LiteralValue::Number {
3931                value: angle.angle.value,
3932                suffix: angle.angle.units,
3933            },
3934            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3935                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3936            digest: None,
3937        })));
3938
3939        Ok((call, value))
3940    }
3941
3942    async fn add_tangent(
3943        &mut self,
3944        sketch: ObjectId,
3945        tangent: Tangent,
3946        new_ast: &mut ast::Node<ast::Program>,
3947    ) -> Result<AstNodeRef, KclError> {
3948        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3949            return Err(KclError::refactor(format!(
3950                "Tangent constraint must have exactly 2 segments, got {}",
3951                tangent.input.len()
3952            )));
3953        };
3954        let sketch_id = sketch;
3955
3956        let seg0_object = self
3957            .scene_graph
3958            .objects
3959            .get(seg0_id.0)
3960            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3961        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3962            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3963        };
3964        let seg0_ast = match seg0_segment {
3965            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3966                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3967            }
3968            _ => {
3969                return Err(KclError::refactor(format!(
3970                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3971                )));
3972            }
3973        };
3974
3975        let seg1_object = self
3976            .scene_graph
3977            .objects
3978            .get(seg1_id.0)
3979            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3980        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3981            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3982        };
3983        let seg1_ast = match seg1_segment {
3984            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3985                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3986            }
3987            _ => {
3988                return Err(KclError::refactor(format!(
3989                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3990                )));
3991            }
3992        };
3993
3994        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3995        let (sketch_block_ref, _) = self.mutate_ast(
3996            new_ast,
3997            sketch_id,
3998            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3999        )?;
4000        Ok(sketch_block_ref)
4001    }
4002
4003    async fn add_symmetric(
4004        &mut self,
4005        sketch: ObjectId,
4006        symmetric: Symmetric,
4007        new_ast: &mut ast::Node<ast::Program>,
4008    ) -> Result<AstNodeRef, KclError> {
4009        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4010            return Err(KclError::refactor(format!(
4011                "Symmetric constraint must have exactly 2 inputs, got {}",
4012                symmetric.input.len()
4013            )));
4014        };
4015        let sketch_id = sketch;
4016
4017        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4018        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4019        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4020
4021        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4022        let (sketch_block_ref, _) = self.mutate_ast(
4023            new_ast,
4024            sketch_id,
4025            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4026        )?;
4027        Ok(sketch_block_ref)
4028    }
4029
4030    async fn add_midpoint(
4031        &mut self,
4032        sketch: ObjectId,
4033        midpoint: Midpoint,
4034        new_ast: &mut ast::Node<ast::Program>,
4035    ) -> Result<AstNodeRef, KclError> {
4036        let sketch_id = sketch;
4037        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4038
4039        let segment_object = self
4040            .scene_graph
4041            .objects
4042            .get(midpoint.segment.0)
4043            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4044        let ObjectKind::Segment {
4045            segment: midpoint_segment,
4046        } = &segment_object.kind
4047        else {
4048            return Err(KclError::refactor(format!(
4049                "Object must be a segment, but it was {}",
4050                segment_object.kind.human_friendly_kind_with_article()
4051            )));
4052        };
4053        let segment_ast = match midpoint_segment {
4054            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4055            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4056            _ => {
4057                return Err(KclError::refactor(format!(
4058                    "Midpoint target must be a line or arc segment but it was {}",
4059                    midpoint_segment.human_friendly_kind_with_article()
4060                )));
4061            }
4062        };
4063
4064        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4065        let (sketch_block_ref, _) = self.mutate_ast(
4066            new_ast,
4067            sketch_id,
4068            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4069        )?;
4070        Ok(sketch_block_ref)
4071    }
4072
4073    async fn add_equal_radius(
4074        &mut self,
4075        sketch: ObjectId,
4076        equal_radius: EqualRadius,
4077        new_ast: &mut ast::Node<ast::Program>,
4078    ) -> Result<AstNodeRef, KclError> {
4079        if equal_radius.input.len() < 2 {
4080            return Err(KclError::refactor(format!(
4081                "equalRadius constraint must have at least 2 segments, got {}",
4082                equal_radius.input.len()
4083            )));
4084        }
4085
4086        let sketch_id = sketch;
4087        let input_asts = equal_radius
4088            .input
4089            .iter()
4090            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4091            .collect::<Result<Vec<_>, _>>()?;
4092
4093        let equal_radius_ast = create_equal_radius_ast(input_asts);
4094        let (sketch_block_ref, _) = self.mutate_ast(
4095            new_ast,
4096            sketch_id,
4097            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4098        )?;
4099        Ok(sketch_block_ref)
4100    }
4101
4102    async fn add_radius(
4103        &mut self,
4104        sketch: ObjectId,
4105        radius: Radius,
4106        new_ast: &mut ast::Node<ast::Program>,
4107    ) -> Result<AstNodeRef, KclError> {
4108        let params = ArcSizeConstraintParams {
4109            points: vec![radius.arc],
4110            function_name: RADIUS_FN,
4111            value: radius.radius.value,
4112            units: radius.radius.units,
4113            label_position: radius.label_position,
4114            constraint_type_name: "Radius",
4115        };
4116        self.add_arc_size_constraint(sketch, params, new_ast).await
4117    }
4118
4119    async fn add_diameter(
4120        &mut self,
4121        sketch: ObjectId,
4122        diameter: Diameter,
4123        new_ast: &mut ast::Node<ast::Program>,
4124    ) -> Result<AstNodeRef, KclError> {
4125        let params = ArcSizeConstraintParams {
4126            points: vec![diameter.arc],
4127            function_name: DIAMETER_FN,
4128            value: diameter.diameter.value,
4129            units: diameter.diameter.units,
4130            label_position: diameter.label_position,
4131            constraint_type_name: "Diameter",
4132        };
4133        self.add_arc_size_constraint(sketch, params, new_ast).await
4134    }
4135
4136    async fn add_fixed_constraints(
4137        &mut self,
4138        sketch: ObjectId,
4139        points: Vec<FixedPoint>,
4140        new_ast: &mut ast::Node<ast::Program>,
4141    ) -> Result<AstNodeRef, KclError> {
4142        let mut sketch_block_ref = None;
4143
4144        for fixed_point in points {
4145            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4146            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4147                .map_err(|err| KclError::refactor(err.to_string()))?;
4148
4149            let (sketch_ref, _) = self.mutate_ast(
4150                new_ast,
4151                sketch,
4152                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4153            )?;
4154            sketch_block_ref = Some(sketch_ref);
4155        }
4156
4157        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4158    }
4159
4160    async fn add_arc_size_constraint(
4161        &mut self,
4162        sketch: ObjectId,
4163        params: ArcSizeConstraintParams,
4164        new_ast: &mut ast::Node<ast::Program>,
4165    ) -> Result<AstNodeRef, KclError> {
4166        let sketch_id = sketch;
4167
4168        // Constraint must have exactly 1 argument (arc segment)
4169        if params.points.len() != 1 {
4170            return Err(KclError::refactor(format!(
4171                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4172                params.constraint_type_name,
4173                params.points.len()
4174            )));
4175        }
4176
4177        let arc_id = params.points[0];
4178        let arc_object = self
4179            .scene_graph
4180            .objects
4181            .get(arc_id.0)
4182            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4183        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4184            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4185        };
4186        let ref_type = match arc_segment {
4187            Segment::Arc(_) => ARC_VARIABLE,
4188            Segment::Circle(_) => CIRCLE_VARIABLE,
4189            _ => {
4190                return Err(KclError::refactor(format!(
4191                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4192                    params.constraint_type_name
4193                )));
4194            }
4195        };
4196        // Reference the arc/circle segment directly
4197        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4198        let arguments = match &params.label_position {
4199            Some(label_position) => vec![ast::LabeledArg {
4200                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4201                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4202            }],
4203            None => Default::default(),
4204        };
4205
4206        // Create the function call.
4207        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4208            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4209            unlabeled: Some(arc_ast),
4210            arguments,
4211            digest: None,
4212            non_code_meta: Default::default(),
4213        })));
4214        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4215            left: call_ast,
4216            operator: ast::BinaryOperator::Eq,
4217            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4218                value: ast::LiteralValue::Number {
4219                    value: params.value,
4220                    suffix: params.units,
4221                },
4222                raw: format_number_literal(params.value, params.units, None)
4223                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4224                digest: None,
4225            }))),
4226            digest: None,
4227        })));
4228
4229        // Add the line to the AST of the sketch block.
4230        let (sketch_block_ref, _) = self.mutate_ast(
4231            new_ast,
4232            sketch_id,
4233            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4234        )?;
4235        Ok(sketch_block_ref)
4236    }
4237
4238    async fn add_horizontal_distance(
4239        &mut self,
4240        sketch: ObjectId,
4241        distance: Distance,
4242        new_ast: &mut ast::Node<ast::Program>,
4243    ) -> Result<AstNodeRef, KclError> {
4244        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4245    }
4246
4247    async fn add_vertical_distance(
4248        &mut self,
4249        sketch: ObjectId,
4250        distance: Distance,
4251        new_ast: &mut ast::Node<ast::Program>,
4252    ) -> Result<AstNodeRef, KclError> {
4253        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4254    }
4255
4256    async fn add_horizontal(
4257        &mut self,
4258        sketch: ObjectId,
4259        horizontal: Horizontal,
4260        new_ast: &mut ast::Node<ast::Program>,
4261    ) -> Result<AstNodeRef, KclError> {
4262        let sketch_id = sketch;
4263
4264        // Map the runtime objects back to variable names.
4265        let first_arg_ast = match horizontal {
4266            Horizontal::Line { line } => {
4267                let line_object = self
4268                    .scene_graph
4269                    .objects
4270                    .get(line.0)
4271                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4272                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4273                    let kind = line_object.kind.human_friendly_kind_with_article();
4274                    return Err(KclError::refactor(format!(
4275                        "This constraint only works on Segments, but you selected {kind}"
4276                    )));
4277                };
4278                let Segment::Line(_) = line_segment else {
4279                    return Err(KclError::refactor(format!(
4280                        "Only lines can be made horizontal, but you selected {}",
4281                        line_segment.human_friendly_kind_with_article(),
4282                    )));
4283                };
4284                self.line_id_to_ast_reference(line, new_ast)?
4285            }
4286            Horizontal::Points { points } => {
4287                let point_asts = points
4288                    .iter()
4289                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4290                    .collect::<Result<Vec<_>, _>>()?;
4291                ast::ArrayExpression::new(point_asts).into()
4292            }
4293        };
4294        // Create the horizontal() call using shared helper.
4295        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4296
4297        // Add the line to the AST of the sketch block.
4298        let (sketch_block_ref, _) = self.mutate_ast(
4299            new_ast,
4300            sketch_id,
4301            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4302        )?;
4303        Ok(sketch_block_ref)
4304    }
4305
4306    async fn add_lines_equal_length(
4307        &mut self,
4308        sketch: ObjectId,
4309        lines_equal_length: LinesEqualLength,
4310        new_ast: &mut ast::Node<ast::Program>,
4311    ) -> Result<AstNodeRef, KclError> {
4312        if lines_equal_length.lines.len() < 2 {
4313            return Err(KclError::refactor(format!(
4314                "Lines equal length constraint must have at least 2 lines, got {}",
4315                lines_equal_length.lines.len()
4316            )));
4317        };
4318
4319        let sketch_id = sketch;
4320
4321        // Map the runtime objects back to variable names.
4322        let line_asts = lines_equal_length
4323            .lines
4324            .iter()
4325            .map(|line_id| {
4326                let line_object = self
4327                    .scene_graph
4328                    .objects
4329                    .get(line_id.0)
4330                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332                    let kind = line_object.kind.human_friendly_kind_with_article();
4333                    return Err(KclError::refactor(format!(
4334                        "This constraint only works on Segments, but you selected {kind}"
4335                    )));
4336                };
4337                let Segment::Line(_) = line_segment else {
4338                    let kind = line_segment.human_friendly_kind_with_article();
4339                    return Err(KclError::refactor(format!(
4340                        "Only lines can be made equal length, but you selected {kind}"
4341                    )));
4342                };
4343
4344                self.line_id_to_ast_reference(*line_id, new_ast)
4345            })
4346            .collect::<Result<Vec<_>, _>>()?;
4347
4348        // Create the equalLength() call using shared helper.
4349        let equal_length_ast = create_equal_length_ast(line_asts);
4350
4351        // Add the constraint to the AST of the sketch block.
4352        let (sketch_block_ref, _) = self.mutate_ast(
4353            new_ast,
4354            sketch_id,
4355            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4356        )?;
4357        Ok(sketch_block_ref)
4358    }
4359
4360    fn equal_radius_segment_id_to_ast_reference(
4361        &mut self,
4362        segment_id: ObjectId,
4363        new_ast: &mut ast::Node<ast::Program>,
4364    ) -> Result<ast::Expr, KclError> {
4365        let segment_object = self
4366            .scene_graph
4367            .objects
4368            .get(segment_id.0)
4369            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4370        let ObjectKind::Segment { segment } = &segment_object.kind else {
4371            return Err(KclError::refactor(format!(
4372                "Object is not a segment, it was {}",
4373                segment_object.kind.human_friendly_kind_with_article()
4374            )));
4375        };
4376
4377        let ref_type = match segment {
4378            Segment::Arc(_) => ARC_VARIABLE,
4379            Segment::Circle(_) => CIRCLE_VARIABLE,
4380            _ => {
4381                return Err(KclError::refactor(format!(
4382                    "equalRadius supports only arc/circle segments, got {}",
4383                    segment.human_friendly_kind_with_article()
4384                )));
4385            }
4386        };
4387
4388        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4389    }
4390
4391    fn symmetric_input_id_to_ast_reference(
4392        &mut self,
4393        segment_id: ObjectId,
4394        new_ast: &mut ast::Node<ast::Program>,
4395    ) -> Result<ast::Expr, KclError> {
4396        let segment_object = self
4397            .scene_graph
4398            .objects
4399            .get(segment_id.0)
4400            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4401        let ObjectKind::Segment { segment } = &segment_object.kind else {
4402            return Err(KclError::refactor(format!(
4403                "Object is not a segment, it was {}",
4404                segment_object.kind.human_friendly_kind_with_article()
4405            )));
4406        };
4407
4408        match segment {
4409            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4410            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4411            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4412            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4413            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4414                "Symmetric does not yet support control point splines".to_owned(),
4415            )),
4416        }
4417    }
4418
4419    fn symmetric_axis_id_to_ast_reference(
4420        &mut self,
4421        segment_id: ObjectId,
4422        new_ast: &mut ast::Node<ast::Program>,
4423    ) -> Result<ast::Expr, KclError> {
4424        let segment_object = self
4425            .scene_graph
4426            .objects
4427            .get(segment_id.0)
4428            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4429        let ObjectKind::Segment { segment } = &segment_object.kind else {
4430            return Err(KclError::refactor(format!(
4431                "Object is not a segment, it was {}",
4432                segment_object.kind.human_friendly_kind_with_article()
4433            )));
4434        };
4435        match segment {
4436            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4437            _ => Err(KclError::refactor(format!(
4438                "Symmetric axis must be a line, got {}",
4439                segment.human_friendly_kind_with_article()
4440            ))),
4441        }
4442    }
4443
4444    async fn add_parallel(
4445        &mut self,
4446        sketch: ObjectId,
4447        parallel: Parallel,
4448        new_ast: &mut ast::Node<ast::Program>,
4449    ) -> Result<AstNodeRef, KclError> {
4450        if parallel.lines.len() < 2 {
4451            return Err(KclError::refactor(format!(
4452                "Parallel constraint must have at least 2 lines, got {}",
4453                parallel.lines.len()
4454            )));
4455        };
4456
4457        let sketch_id = sketch;
4458
4459        let line_asts = parallel
4460            .lines
4461            .iter()
4462            .map(|line_id| {
4463                let line_object = self
4464                    .scene_graph
4465                    .objects
4466                    .get(line_id.0)
4467                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4468                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4469                    let kind = line_object.kind.human_friendly_kind_with_article();
4470                    return Err(KclError::refactor(format!(
4471                        "This constraint only works on Segments, but you selected {kind}"
4472                    )));
4473                };
4474                let Segment::Line(_) = line_segment else {
4475                    let kind = line_segment.human_friendly_kind_with_article();
4476                    return Err(KclError::refactor(format!(
4477                        "Only lines can be made parallel, but you selected {kind}"
4478                    )));
4479                };
4480
4481                self.line_id_to_ast_reference(*line_id, new_ast)
4482            })
4483            .collect::<Result<Vec<_>, _>>()?;
4484
4485        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4486            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4487            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4488                ast::ArrayExpression {
4489                    elements: line_asts,
4490                    digest: None,
4491                    non_code_meta: Default::default(),
4492                },
4493            )))),
4494            arguments: Default::default(),
4495            digest: None,
4496            non_code_meta: Default::default(),
4497        })));
4498
4499        let (sketch_block_ref, _) = self.mutate_ast(
4500            new_ast,
4501            sketch_id,
4502            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4503        )?;
4504        Ok(sketch_block_ref)
4505    }
4506
4507    async fn add_perpendicular(
4508        &mut self,
4509        sketch: ObjectId,
4510        perpendicular: Perpendicular,
4511        new_ast: &mut ast::Node<ast::Program>,
4512    ) -> Result<AstNodeRef, KclError> {
4513        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4514            .await
4515    }
4516
4517    async fn add_lines_at_angle_constraint(
4518        &mut self,
4519        sketch: ObjectId,
4520        angle_kind: LinesAtAngleKind,
4521        lines: Vec<ObjectId>,
4522        new_ast: &mut ast::Node<ast::Program>,
4523    ) -> Result<AstNodeRef, KclError> {
4524        let &[line0_id, line1_id] = lines.as_slice() else {
4525            return Err(KclError::refactor(format!(
4526                "{} constraint must have exactly 2 lines, got {}",
4527                angle_kind.to_function_name(),
4528                lines.len()
4529            )));
4530        };
4531
4532        let sketch_id = sketch;
4533
4534        // Map the runtime objects back to variable names.
4535        let line0_object = self
4536            .scene_graph
4537            .objects
4538            .get(line0_id.0)
4539            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4540        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4541            let kind = line0_object.kind.human_friendly_kind_with_article();
4542            return Err(KclError::refactor(format!(
4543                "This constraint only works on Segments, but you selected {kind}"
4544            )));
4545        };
4546        let Segment::Line(_) = line0_segment else {
4547            return Err(KclError::refactor(format!(
4548                "Only lines can be made {}, but you selected {}",
4549                angle_kind.to_function_name(),
4550                line0_segment.human_friendly_kind_with_article(),
4551            )));
4552        };
4553        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4554
4555        let line1_object = self
4556            .scene_graph
4557            .objects
4558            .get(line1_id.0)
4559            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4560        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4561            let kind = line1_object.kind.human_friendly_kind_with_article();
4562            return Err(KclError::refactor(format!(
4563                "This constraint only works on Segments, but you selected {kind}"
4564            )));
4565        };
4566        let Segment::Line(_) = line1_segment else {
4567            return Err(KclError::refactor(format!(
4568                "Only lines can be made {}, but you selected {}",
4569                angle_kind.to_function_name(),
4570                line1_segment.human_friendly_kind_with_article(),
4571            )));
4572        };
4573        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4574
4575        // Create the parallel() or perpendicular() call.
4576        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4577            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4578            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4579                ast::ArrayExpression {
4580                    elements: vec![line0_ast, line1_ast],
4581                    digest: None,
4582                    non_code_meta: Default::default(),
4583                },
4584            )))),
4585            arguments: Default::default(),
4586            digest: None,
4587            non_code_meta: Default::default(),
4588        })));
4589
4590        // Add the constraint to the AST of the sketch block.
4591        let (sketch_block_ref, _) = self.mutate_ast(
4592            new_ast,
4593            sketch_id,
4594            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4595        )?;
4596        Ok(sketch_block_ref)
4597    }
4598
4599    async fn add_vertical(
4600        &mut self,
4601        sketch: ObjectId,
4602        vertical: Vertical,
4603        new_ast: &mut ast::Node<ast::Program>,
4604    ) -> Result<AstNodeRef, KclError> {
4605        let sketch_id = sketch;
4606
4607        let first_arg_ast = match vertical {
4608            Vertical::Line { line } => {
4609                // Map the runtime objects back to variable names.
4610                let line_object = self
4611                    .scene_graph
4612                    .objects
4613                    .get(line.0)
4614                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4615                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4616                    let kind = line_object.kind.human_friendly_kind_with_article();
4617                    return Err(KclError::refactor(format!(
4618                        "This constraint only works on Segments, but you selected {kind}"
4619                    )));
4620                };
4621                let Segment::Line(_) = line_segment else {
4622                    return Err(KclError::refactor(format!(
4623                        "Only lines can be made vertical, but you selected {}",
4624                        line_segment.human_friendly_kind_with_article()
4625                    )));
4626                };
4627                self.line_id_to_ast_reference(line, new_ast)?
4628            }
4629            Vertical::Points { points } => {
4630                let point_asts = points
4631                    .iter()
4632                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4633                    .collect::<Result<Vec<_>, _>>()?;
4634                ast::ArrayExpression::new(point_asts).into()
4635            }
4636        };
4637        // Create the vertical() call using shared helper.
4638        let vertical_ast = create_vertical_ast(first_arg_ast);
4639
4640        // Add the line to the AST of the sketch block.
4641        let (sketch_block_ref, _) = self.mutate_ast(
4642            new_ast,
4643            sketch_id,
4644            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4645        )?;
4646        Ok(sketch_block_ref)
4647    }
4648
4649    async fn execute_after_add_constraint(
4650        &mut self,
4651        ctx: &ExecutorContext,
4652        sketch_id: ObjectId,
4653        sketch_block_ref: AstNodeRef,
4654        new_ast: &mut ast::Node<ast::Program>,
4655    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4656        // Convert to string source to create real source ranges.
4657        let new_source = source_from_ast(new_ast);
4658        // Parse the new KCL source.
4659        let new_program = parse_frontend_mutation_source(
4660            &new_source,
4661            "Error parsing KCL source after adding constraint",
4662            "No AST produced after adding constraint",
4663        )?;
4664        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4665            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4666                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4667            )))
4668        })?;
4669
4670        // Truncate after the sketch block for mock execution.
4671        // Use a clone so we don't mutate new_program yet
4672        let mut truncated_program = new_program.clone();
4673        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4674            .map_err(KclErrorWithOutputs::no_outputs)?;
4675
4676        // Execute - if this fails, we haven't modified self yet, so state is safe
4677        let outcome = ctx
4678            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4679            .await?;
4680
4681        let new_object_ids = {
4682            // Extract the constraint ID from the execution outcome using source_range_to_object
4683            let constraint_id = outcome
4684                .source_range_to_object
4685                .get(&constraint_node_ref.range)
4686                .copied()
4687                .ok_or_else(|| {
4688                    KclErrorWithOutputs::from_error_outcome(
4689                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4690                        outcome.clone(),
4691                    )
4692                })?;
4693            vec![constraint_id]
4694        };
4695
4696        // Only now, after all operations succeeded, update self.program.
4697        // This ensures state is only modified if everything succeeds.
4698        self.program = new_program;
4699
4700        // Uses MockConfig::default() which has freedom_analysis: true
4701        let outcome = self.update_state_after_exec(outcome, true);
4702
4703        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4704        let scene_graph_delta = SceneGraphDelta {
4705            new_graph: self.scene_graph_for_ui(),
4706            invalidates_ids: false,
4707            new_objects: new_object_ids,
4708            exec_outcome: outcome,
4709        };
4710        Ok((src_delta, scene_graph_delta))
4711    }
4712
4713    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4714        let commit_failure = || {
4715            KclErrorWithOutputs::from_error_outcome(
4716                KclError::refactor(format!("Could not update KCL after {operation}.")),
4717                outcome.clone(),
4718            )
4719        };
4720
4721        let default_length_unit = self.default_length_unit();
4722        let mut settled_ast = self.program.ast.clone();
4723        let mut committed_solver_value = false;
4724        for (var_range, node_path, value) in &outcome.var_solutions {
4725            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4726                return Err(commit_failure());
4727            };
4728            let new_value = match &lookup {
4729                Some(current_literal) => {
4730                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4731                        continue;
4732                    }
4733                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4734                }
4735                None => {
4736                    // Bare `var` with no initial literal to compare against;
4737                    // always commit, using the module's default length unit as
4738                    // an explicit suffix so the written value carries units.
4739                    Number {
4740                        value: number_value_in_default_length_units(*value, default_length_unit),
4741                        units: default_length_unit.into(),
4742                    }
4743                }
4744            };
4745            committed_solver_value = true;
4746            let source_ref = SourceRef::Simple {
4747                range: *var_range,
4748                node_path: node_path.clone(),
4749            };
4750            mutate_ast_node_by_source_ref(
4751                &mut settled_ast,
4752                &source_ref,
4753                AstMutateCommand::EditVarInitialValue { value: new_value },
4754            )
4755            .map_err(|_| commit_failure())?;
4756        }
4757
4758        if !committed_solver_value {
4759            return Ok(SourceDelta {
4760                text: self.program.original_file_contents.clone(),
4761            });
4762        }
4763
4764        let settled_source = source_from_ast(&settled_ast);
4765        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4766        if !errors.is_empty() {
4767            return Err(commit_failure());
4768        }
4769        let Some(settled_program) = settled_program else {
4770            return Err(commit_failure());
4771        };
4772
4773        self.program = settled_program;
4774
4775        Ok(SourceDelta { text: settled_source })
4776    }
4777
4778    // Find constraints that reference the given segments.
4779    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4780        if segment_ids_set.contains(&segment_id) {
4781            return true;
4782        }
4783
4784        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4785            return false;
4786        };
4787        let ObjectKind::Segment { segment } = &segment_object.kind else {
4788            return false;
4789        };
4790        let Segment::Point(point) = segment else {
4791            return false;
4792        };
4793
4794        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4795    }
4796
4797    fn remaining_constraint_segments(
4798        &self,
4799        segments: &[ConstraintSegment],
4800        segment_ids_set: &AhashIndexSet<ObjectId>,
4801    ) -> Vec<ConstraintSegment> {
4802        segments
4803            .iter()
4804            .copied()
4805            .filter(|segment| match segment {
4806                ConstraintSegment::Origin(_) => true,
4807                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4808            })
4809            .collect()
4810    }
4811
4812    fn find_referenced_constraints(
4813        &self,
4814        sketch_id: ObjectId,
4815        segment_ids_set: &AhashIndexSet<ObjectId>,
4816    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4817        // Look up the sketch.
4818        let sketch_object = self
4819            .scene_graph
4820            .objects
4821            .get(sketch_id.0)
4822            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4823        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4824            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4825        };
4826        let segment_or_owner_matches = |segment_id: ObjectId| {
4827            if segment_ids_set.contains(&segment_id) {
4828                return true;
4829            }
4830            let segment_object = self.scene_graph.objects.get(segment_id.0);
4831            if let Some(obj) = segment_object
4832                && let ObjectKind::Segment { segment } = &obj.kind
4833            {
4834                match segment {
4835                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4836                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4837                    _ => false,
4838                }
4839            } else {
4840                false
4841            }
4842        };
4843        let mut constraint_ids_set = AhashIndexSet::default();
4844        for constraint_id in &sketch.constraints {
4845            let constraint_object = self
4846                .scene_graph
4847                .objects
4848                .get(constraint_id.0)
4849                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4850            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4851                return Err(KclError::refactor(format!(
4852                    "Object is not a constraint, it is {}",
4853                    constraint_object.kind.human_friendly_kind_with_article()
4854                )));
4855            };
4856            let depends_on_segment = match constraint {
4857                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4858                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4859                Constraint::Fixed(fixed) => fixed
4860                    .points
4861                    .iter()
4862                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4863                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4864                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4865                Constraint::EqualRadius(equal_radius) => {
4866                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4867                }
4868                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4869                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4870                Constraint::Horizontal(h) => match h {
4871                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4872                    Horizontal::Points { points } => points.iter().any(|point| match point {
4873                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4874                        ConstraintSegment::Origin(_) => false,
4875                    }),
4876                },
4877                Constraint::Vertical(v) => match v {
4878                    Vertical::Line { line } => segment_or_owner_matches(*line),
4879                    Vertical::Points { points } => points.iter().any(|point| match point {
4880                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4881                        ConstraintSegment::Origin(_) => false,
4882                    }),
4883                },
4884                Constraint::LinesEqualLength(lines_equal_length) => {
4885                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4886                }
4887                Constraint::Midpoint(midpoint) => {
4888                    segment_or_owner_matches(midpoint.segment)
4889                        || matches!(
4890                            midpoint.point,
4891                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4892                        )
4893                }
4894                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4895                Constraint::Perpendicular(perpendicular) => {
4896                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4897                }
4898                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4899                Constraint::Symmetric(symmetric) => {
4900                    segment_or_owner_matches(symmetric.axis)
4901                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4902                }
4903                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4904            };
4905            if depends_on_segment {
4906                constraint_ids_set.insert(*constraint_id);
4907            }
4908        }
4909        Ok(constraint_ids_set)
4910    }
4911
4912    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4913        let mut outcome = outcome;
4914        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4915        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4916
4917        if freedom_analysis_ran {
4918            // When freedom analysis ran, replace the cache entirely with new values
4919            // Don't merge with old values since IDs might have changed
4920            self.point_freedom_cache.clear();
4921            for new_obj in &new_objects {
4922                if let ObjectKind::Segment {
4923                    segment: crate::front::Segment::Point(point),
4924                } = &new_obj.kind
4925                {
4926                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4927                }
4928            }
4929            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4930            // Objects are already correct from the analysis, just use them as-is
4931            self.scene_graph.objects = new_objects;
4932        } else {
4933            // When freedom analysis didn't run, preserve old values and merge
4934            // Before replacing objects, extract and store freedom values from old objects
4935            for old_obj in &self.scene_graph.objects {
4936                if let ObjectKind::Segment {
4937                    segment: crate::front::Segment::Point(point),
4938                } = &old_obj.kind
4939                {
4940                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4941                }
4942            }
4943
4944            // Update objects, preserving stored freedom values when new is Free (might be default)
4945            let mut updated_objects = Vec::with_capacity(new_objects.len());
4946            for new_obj in new_objects {
4947                let mut obj = new_obj;
4948                if let ObjectKind::Segment {
4949                    segment: crate::front::Segment::Point(point),
4950                } = &mut obj.kind
4951                {
4952                    let new_freedom = point.freedom;
4953                    // When freedom_analysis=false, new values are defaults (Free).
4954                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4955                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4956                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4957                    // when there are actually unsatisfied constraints.
4958                    match new_freedom {
4959                        Freedom::Free => {
4960                            match self.point_freedom_cache.get(&obj.id).copied() {
4961                                Some(Freedom::Conflict) => {
4962                                    // Don't preserve Conflict - conflicts are transient
4963                                    // Keep it as Free
4964                                }
4965                                Some(Freedom::Fixed) => {
4966                                    // Preserve Fixed cached value
4967                                    point.freedom = Freedom::Fixed;
4968                                }
4969                                Some(Freedom::Free) => {
4970                                    // If stored is also Free, keep Free (no change needed)
4971                                }
4972                                None => {
4973                                    // If no cached value, keep Free (default)
4974                                }
4975                            }
4976                        }
4977                        Freedom::Fixed => {
4978                            // Use new value (already set)
4979                        }
4980                        Freedom::Conflict => {
4981                            // Use new value (already set)
4982                        }
4983                    }
4984                    // Store the new freedom value (even if it's Free, so we know it was set)
4985                    self.point_freedom_cache.insert(obj.id, point.freedom);
4986                }
4987                updated_objects.push(obj);
4988            }
4989
4990            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4991            self.scene_graph.objects = updated_objects;
4992        }
4993        outcome
4994    }
4995
4996    fn mutate_ast(
4997        &mut self,
4998        ast: &mut ast::Node<ast::Program>,
4999        object_id: ObjectId,
5000        command: AstMutateCommand,
5001    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5002        let sketch_object = self
5003            .scene_graph
5004            .objects
5005            .get(object_id.0)
5006            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5007        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5008    }
5009
5010    fn mutate_constraint_label_position(
5011        &mut self,
5012        ast: &mut ast::Node<ast::Program>,
5013        constraint_id: ObjectId,
5014        label_position: Point2d<Number>,
5015    ) -> Result<(), KclError> {
5016        let object = self
5017            .scene_graph
5018            .objects
5019            .get(constraint_id.0)
5020            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5021        if !matches!(
5022            &object.kind,
5023            ObjectKind::Constraint {
5024                constraint: Constraint::Distance(_)
5025                    | Constraint::HorizontalDistance(_)
5026                    | Constraint::VerticalDistance(_)
5027                    | Constraint::Radius(_)
5028                    | Constraint::Diameter(_)
5029                    | Constraint::Angle(_),
5030            }
5031        ) {
5032            return Err(KclError::refactor(format!(
5033                "Object does not support labelPosition: {constraint_id:?}"
5034            )));
5035        }
5036
5037        let label_position = to_ast_point2d_number(&label_position)
5038            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5039        self.mutate_ast(
5040            ast,
5041            constraint_id,
5042            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5043        )?;
5044        Ok(())
5045    }
5046}
5047
5048fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5049    // Look up existing sketch.
5050    let sketch_object = scene_graph
5051        .objects
5052        .get(sketch_id.0)
5053        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5054    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5055        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5056    };
5057    expect_single_node_ref(sketch_object)
5058}
5059
5060fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5061    match &object.source {
5062        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5063            range: *range,
5064            node_path: node_path.clone(),
5065        }),
5066        SourceRef::BackTrace { ranges } => {
5067            let [range] = ranges.as_slice() else {
5068                return Err(KclError::refactor(format!(
5069                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5070                    ranges.len()
5071                )));
5072            };
5073            Ok(AstNodeRef {
5074                range: range.0,
5075                node_path: range.1.clone(),
5076            })
5077        }
5078    }
5079}
5080
5081/// This is a deprecated fall-back implementation. Prefer
5082/// [`only_sketch_block()`] to avoid reliance on source ranges.
5083fn only_sketch_block_from_range(
5084    ast: &mut ast::Node<ast::Program>,
5085    sketch_block_range: SourceRange,
5086    edit_kind: ChangeKind,
5087) -> Result<(), KclError> {
5088    let r1 = sketch_block_range;
5089    let matches_range = |r2: SourceRange| -> bool {
5090        // We may have added items to the sketch block, so the end may not be an
5091        // exact match.
5092        match edit_kind {
5093            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5094            // For edit, we don't know whether it grew or shrank.
5095            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5096            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5097            // No edit should be an exact match.
5098            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5099        }
5100    };
5101    let mut found = false;
5102    for item in ast.body.iter_mut() {
5103        match item {
5104            ast::BodyItem::ImportStatement(_) => {}
5105            ast::BodyItem::ExpressionStatement(node) => {
5106                if matches_range(SourceRange::from(&*node))
5107                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5108                {
5109                    sketch_block.is_being_edited = true;
5110                    found = true;
5111                    break;
5112                }
5113            }
5114            ast::BodyItem::VariableDeclaration(node) => {
5115                if matches_range(SourceRange::from(&node.declaration.init))
5116                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5117                {
5118                    sketch_block.is_being_edited = true;
5119                    found = true;
5120                    break;
5121                }
5122            }
5123            ast::BodyItem::TypeDeclaration(_) => {}
5124            ast::BodyItem::ReturnStatement(node) => {
5125                if matches_range(SourceRange::from(&node.argument))
5126                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5127                {
5128                    sketch_block.is_being_edited = true;
5129                    found = true;
5130                    break;
5131                }
5132            }
5133        }
5134    }
5135    if !found {
5136        return Err(KclError::refactor(format!(
5137            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5138        )));
5139    }
5140
5141    Ok(())
5142}
5143
5144fn only_sketch_block(
5145    ast: &mut ast::Node<ast::Program>,
5146    sketch_block_ref: &AstNodeRef,
5147    edit_kind: ChangeKind,
5148) -> Result<(), KclError> {
5149    let Some(target_node_path) = &sketch_block_ref.node_path else {
5150        #[cfg(target_arch = "wasm32")]
5151        web_sys::console::warn_1(
5152            &format!(
5153                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5154                sketch_block_ref
5155            )
5156            .into(),
5157        );
5158        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5159    };
5160    struct MarkSketchBlockBeingEdited<'a> {
5161        target_node_path: &'a ast::NodePath,
5162    }
5163
5164    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5165        type Break = ();
5166        type Continue = ();
5167
5168        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5169            if let NodeMut::SketchBlock(sketch_block) = node
5170                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5171            {
5172                sketch_block.is_being_edited = true;
5173                return TraversalReturn::new_break(());
5174            }
5175            TraversalReturn::new_continue(())
5176        }
5177
5178        fn finish(&mut self, _node: NodeMut<'_>) {}
5179    }
5180
5181    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5182    let found = dfs_mut(ast, &mut marker).is_break();
5183    if !found {
5184        return Err(KclError::refactor(format!(
5185            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5186        )));
5187    }
5188
5189    Ok(())
5190}
5191
5192fn sketch_on_ast_expr(
5193    ast: &mut ast::Node<ast::Program>,
5194    scene_graph: &SceneGraph,
5195    solid_references: &HashMap<Uuid, SolidAstReference>,
5196    on: &Plane,
5197) -> Result<ast::Expr, KclError> {
5198    match on {
5199        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5200        Plane::Object(object_id) => {
5201            let on_object = scene_graph
5202                .objects
5203                .get(object_id.0)
5204                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5205            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5206                return Ok(face_expr);
5207            }
5208            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5209        }
5210        Plane::PrimitiveFace(face) => {
5211            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5212                KclError::refactor(format!(
5213                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5214                    face.solid_id
5215                ))
5216            })?;
5217            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5218            Ok(create_face_of_ast(solid_expr, face_id_expr))
5219        }
5220    }
5221}
5222
5223fn solid_references_from_variables(
5224    ast: &ast::Node<ast::Program>,
5225    variables: &IndexMap<String, KclValueView>,
5226) -> HashMap<Uuid, SolidAstReference> {
5227    let mut references = HashMap::new();
5228
5229    // In-place operations such as shell reuse their input solid's engine ID.
5230    // Later variables overwrite earlier references so mutations target the result.
5231    for item in &ast.body {
5232        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5233            continue;
5234        };
5235        let name = &declaration.declaration.id.name;
5236        let Some(value) = variables.get(name) else {
5237            continue;
5238        };
5239
5240        match value {
5241            KclValueView::Solid { value } => {
5242                references.insert(
5243                    value.id,
5244                    SolidAstReference {
5245                        variable_name: name.clone(),
5246                        output_index: None,
5247                    },
5248                );
5249            }
5250            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5251                for (output_index, entry) in value.iter().enumerate() {
5252                    if let KclValueView::Solid { value } = entry {
5253                        references.insert(
5254                            value.id,
5255                            SolidAstReference {
5256                                variable_name: name.clone(),
5257                                output_index: Some(output_index),
5258                            },
5259                        );
5260                    }
5261                }
5262            }
5263            _ => {}
5264        }
5265    }
5266
5267    references
5268}
5269
5270fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5271    let reference = solid_references.get(&solid_id)?;
5272    let solid_expr = ast_name_expr(reference.variable_name.clone());
5273    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5274}
5275
5276fn sketch_face_of_scene_object_ast_expr(
5277    ast: &mut ast::Node<ast::Program>,
5278    on_object: &crate::front::Object,
5279) -> Result<Option<ast::Expr>, KclError> {
5280    match &on_object.kind {
5281        ObjectKind::Wall(wall) => {
5282            let solid_ref = get_or_insert_ast_reference(
5283                ast,
5284                &source_ref_from_source_ref_range(&wall.source.solid),
5285                "solid",
5286                None,
5287            )?;
5288            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5289                return Err(KclError::refactor(format!(
5290                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5291                    on_object.artifact_id
5292                )));
5293            };
5294            let solid_expr = indexed_solid_expr_for_sweep_output(
5295                ast_name_expr(solid_name_expr.name.name.clone()),
5296                wall.solid_output_index,
5297            );
5298            let sweep_ref = get_or_insert_ast_reference(
5299                ast,
5300                &source_ref_from_source_ref_range(&wall.source.sweep),
5301                "solid",
5302                None,
5303            )?;
5304            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5305                return Err(KclError::refactor(format!(
5306                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5307                    on_object.artifact_id
5308                )));
5309            };
5310            let sweep_name = sweep_name_expr.name.name.clone();
5311            let segment_ref = get_or_insert_ast_reference(
5312                ast,
5313                &source_ref_from_source_ref_range(&wall.source.segment),
5314                LINE_VARIABLE,
5315                None,
5316            )?;
5317
5318            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5319                wall.source
5320                    .path
5321                    .as_ref()
5322                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5323            }) {
5324                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5325                    return Err(KclError::refactor(format!(
5326                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5327                        on_object.artifact_id
5328                    )));
5329                };
5330                create_member_expression(
5331                    create_member_expression(ast_name_expr(region_name), "tags"),
5332                    &segment_name_expr.name.name,
5333                )
5334            } else {
5335                segment_ref
5336            };
5337
5338            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5339        }
5340        ObjectKind::Cap(cap) => {
5341            let solid_ref =
5342                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5343            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5344                return Err(KclError::refactor(format!(
5345                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5346                    on_object.artifact_id
5347                )));
5348            };
5349            let solid_expr = indexed_solid_expr_for_sweep_output(
5350                ast_name_expr(solid_name_expr.name.name.clone()),
5351                cap.solid_output_index,
5352            );
5353            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5354            let face_expr = match cap.kind {
5355                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5356                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5357            };
5358
5359            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5360        }
5361        _ => Ok(None),
5362    }
5363}
5364
5365fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5366    match solid_output_index {
5367        Some(output_index) => create_index_expression(solid_expr, output_index),
5368        None => solid_expr,
5369    }
5370}
5371
5372fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5373    SourceRef::Simple {
5374        range: source.range,
5375        node_path: source.node_path.clone(),
5376    }
5377}
5378
5379fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5380    let source_ref = source_ref_from_source_ref_range(path_source);
5381    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5382    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5383        return None;
5384    };
5385    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5386        return None;
5387    };
5388    if region_call.callee.name.name != "region" {
5389        return None;
5390    }
5391    Some(candidate)
5392}
5393
5394fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5395    let mut current_id = source_id;
5396    let mut current_composite = None;
5397    let mut visited = HashSet::new();
5398
5399    while visited.insert(current_id) {
5400        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5401
5402        let Some(composite_id) = next_composite_id else {
5403            break;
5404        };
5405        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5406            break;
5407        };
5408
5409        current_id = composite.id;
5410        current_composite = Some(composite);
5411
5412        if !composite.consumed {
5413            break;
5414        }
5415    }
5416
5417    current_composite.map(|composite| &composite.code_ref)
5418}
5419
5420fn downstream_composite_id_for_solid_source(
5421    artifact_graph: &ArtifactGraph,
5422    source_id: ArtifactId,
5423) -> Option<ArtifactId> {
5424    // Source is a path, find its solid.
5425    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5426        && let Some(composite_id) = path.composite_solid_id
5427        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5428        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5429    {
5430        return Some(composite_id);
5431    }
5432
5433    // Source is a sweep, find its path -> then find the solid
5434    for artifact in artifact_graph.values() {
5435        if let Artifact::Path(path) = artifact
5436            && path.sweep_id == Some(source_id)
5437            && let Some(composite_id) = path.composite_solid_id
5438            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5439            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5440        {
5441            return Some(composite_id);
5442        }
5443    }
5444
5445    // Source is a solid, find its downstream solid.
5446    artifact_graph.values().find_map(|artifact| {
5447        let Artifact::CompositeSolid(composite) = artifact else {
5448            return None;
5449        };
5450        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5451    })
5452}
5453
5454fn composite_contains_path_input(
5455    solid_ids: &[ArtifactId],
5456    tool_ids: &[ArtifactId],
5457    path_id: ArtifactId,
5458    solid2d_id: Option<ArtifactId>,
5459) -> bool {
5460    composite_contains_input(solid_ids, tool_ids, path_id)
5461        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5462}
5463
5464fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5465    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5466}
5467
5468fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5469    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5470    SourceRefRange {
5471        range: code_ref.range,
5472        node_path,
5473    }
5474}
5475
5476fn solid_output_index_for_sweep(
5477    artifact_graph: &ArtifactGraph,
5478    sweep_id: ArtifactId,
5479    sweep_code_ref: &CodeRef,
5480) -> Option<usize> {
5481    // Constituent sweeps are implementation details of one composite body,
5482    // even when cloning gives them the same CodeRef as the composite root.
5483    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5484        return None;
5485    }
5486
5487    let sibling_sweeps = artifact_graph
5488        .values()
5489        .filter_map(|artifact| match artifact {
5490            Artifact::Sweep(sweep)
5491                if sweep.code_ref.range == sweep_code_ref.range
5492                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5493            {
5494                Some(sweep)
5495            }
5496            _ => None,
5497        })
5498        .collect::<Vec<_>>();
5499
5500    if sibling_sweeps.len() <= 1 {
5501        return None;
5502    }
5503
5504    sibling_sweeps
5505        .iter()
5506        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5507}
5508
5509fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5510    let mut existing_artifact_ids = scene_objects
5511        .iter()
5512        .map(|object| object.artifact_id)
5513        .collect::<HashSet<_>>();
5514
5515    for artifact in artifact_graph.values() {
5516        match artifact {
5517            Artifact::Wall(wall) => {
5518                if existing_artifact_ids.contains(&wall.id) {
5519                    continue;
5520                }
5521
5522                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5523                    Artifact::Segment(segment) => Some(segment),
5524                    _ => None,
5525                }) else {
5526                    continue;
5527                };
5528                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5529                    Artifact::Sweep(sweep) => Some(sweep),
5530                    _ => None,
5531                }) else {
5532                    continue;
5533                };
5534                let source_segment = segment
5535                    .original_seg_id
5536                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5537                    .and_then(|artifact| match artifact {
5538                        Artifact::Segment(segment) => Some(segment),
5539                        _ => None,
5540                    })
5541                    .unwrap_or(segment);
5542                let solid_code_ref =
5543                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5544                let path_code_ref = artifact_graph
5545                    .get(&segment.path_id)
5546                    .or_else(|| artifact_graph.get(&sweep.path_id))
5547                    .and_then(|artifact| match artifact {
5548                        Artifact::Path(path) => Some(&path.code_ref),
5549                        _ => None,
5550                    });
5551                let source = WallSource {
5552                    solid: code_ref_source_ref_range(solid_code_ref),
5553                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5554                    path: path_code_ref.map(code_ref_source_ref_range),
5555                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5556                };
5557                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5558                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5559                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5560                    .flatten();
5561                let object_source = source_ref_from_source_ref_range(&source.solid);
5562                let id = ObjectId(scene_objects.len());
5563                scene_objects.push(crate::front::Object {
5564                    id,
5565                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5566                        id,
5567                        source,
5568                        solid_output_index,
5569                    }),
5570                    label: Default::default(),
5571                    comments: Default::default(),
5572                    artifact_id: wall.id,
5573                    source: object_source,
5574                });
5575                existing_artifact_ids.insert(wall.id);
5576            }
5577            Artifact::Cap(cap) => {
5578                if existing_artifact_ids.contains(&cap.id) {
5579                    continue;
5580                }
5581
5582                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5583                    Artifact::Sweep(sweep) => Some(sweep),
5584                    _ => None,
5585                }) else {
5586                    continue;
5587                };
5588                let id = ObjectId(scene_objects.len());
5589                let kind = match cap.sub_type {
5590                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5591                    CapSubType::End => crate::frontend::api::CapKind::End,
5592                };
5593                let solid_code_ref =
5594                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5595                let source = CapSource {
5596                    solid: code_ref_source_ref_range(solid_code_ref),
5597                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5598                };
5599                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5600                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5601                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5602                    .flatten();
5603                let object_source = source_ref_from_source_ref_range(&source.solid);
5604                scene_objects.push(crate::front::Object {
5605                    id,
5606                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5607                        id,
5608                        kind,
5609                        source,
5610                        solid_output_index,
5611                    }),
5612                    label: Default::default(),
5613                    comments: Default::default(),
5614                    artifact_id: cap.id,
5615                    source: object_source,
5616                });
5617                existing_artifact_ids.insert(cap.id);
5618            }
5619            _ => {}
5620        }
5621    }
5622}
5623
5624fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5625    use crate::engine::PlaneName;
5626
5627    match name {
5628        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5629        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5630        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5631        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5632        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5633        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5634    }
5635}
5636
5637fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5638    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5639        ast::UnaryOperator::Neg,
5640        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5641    )))
5642}
5643
5644fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5645    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5646        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5647        unlabeled: Some(solid_expr),
5648        arguments: vec![ast::LabeledArg {
5649            label: Some(ast::Identifier::new("face")),
5650            arg: face_expr,
5651        }],
5652        digest: None,
5653        non_code_meta: Default::default(),
5654    })))
5655}
5656
5657fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5658    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5660        unlabeled: Some(solid_expr),
5661        arguments: vec![ast::LabeledArg {
5662            label: Some(ast::Identifier::new("index")),
5663            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5664                ast::NumericLiteral {
5665                    value: index as f64,
5666                    suffix: NumericSuffix::None,
5667                    raw: index.to_string(),
5668                    digest: None,
5669                },
5670            )))),
5671        }],
5672        digest: None,
5673        non_code_meta: Default::default(),
5674    })))
5675}
5676
5677fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5678    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5679        return None;
5680    };
5681    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5682        return None;
5683    };
5684    if !matches!(
5685        sweep_call.callee.name.name.as_str(),
5686        "extrude" | "revolve" | "sweep" | "loft"
5687    ) {
5688        return None;
5689    }
5690    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5691        return None;
5692    };
5693    let candidate = region_name_expr.name.name.clone();
5694    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5695        return None;
5696    };
5697    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5698        return None;
5699    };
5700    if region_call.callee.name.name != "region" {
5701        return None;
5702    }
5703    Some(candidate)
5704}
5705
5706/// Return the AST expression referencing the variable at the given source ref.
5707/// If no such variable exists, insert a new variable declaration with the given
5708/// prefix.
5709///
5710/// This may return a complex expression referencing properties of the variable
5711/// (e.g., `line1.start`).
5712fn get_or_insert_ast_reference(
5713    ast: &mut ast::Node<ast::Program>,
5714    source_ref: &SourceRef,
5715    prefix: &str,
5716    property: Option<&str>,
5717) -> Result<ast::Expr, KclError> {
5718    let command = AstMutateCommand::AddVariableDeclaration {
5719        prefix: prefix.to_owned(),
5720    };
5721    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5722        Ok((_, ret)) => ret,
5723        Err(err) => {
5724            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5725                AstMutateCommandReturn::Name(var_name)
5726            } else {
5727                return Err(err);
5728            }
5729        }
5730    };
5731    let AstMutateCommandReturn::Name(var_name) = ret else {
5732        return Err(KclError::refactor(
5733            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5734        ));
5735    };
5736    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5737    let Some(property) = property else {
5738        // No property; just return the variable name.
5739        return Ok(var_expr);
5740    };
5741
5742    Ok(create_member_expression(var_expr, property))
5743}
5744
5745fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5746    let source_range = match source_ref {
5747        SourceRef::Simple { range, .. } => *range,
5748        SourceRef::BackTrace { ranges } => {
5749            let [range] = ranges.as_slice() else {
5750                return None;
5751            };
5752            range.0
5753        }
5754    };
5755    ast.body.iter().find_map(|item| {
5756        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5757            return None;
5758        };
5759        let init_range = SourceRange::from(&var_decl.declaration.init);
5760        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5761            && init_range.start() <= source_range.start()
5762            && source_range.end() <= init_range.end();
5763        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5764            && init_range != source_range
5765            && source_is_inside_init
5766        {
5767            return None;
5768        }
5769        source_is_inside_init.then(|| var_decl.name().to_owned())
5770    })
5771}
5772
5773fn mutate_ast_node_by_source_ref(
5774    ast: &mut ast::Node<ast::Program>,
5775    source_ref: &SourceRef,
5776    command: AstMutateCommand,
5777) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5778    let (source_range, node_path) = match source_ref {
5779        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5780        SourceRef::BackTrace { ranges } => {
5781            let [range] = ranges.as_slice() else {
5782                return Err(KclError::refactor(format!(
5783                    "Expected single source ref, got {}; ranges={ranges:#?}",
5784                    ranges.len(),
5785                )));
5786            };
5787            (range.0, range.1.clone())
5788        }
5789    };
5790    let mut context = AstMutateContext {
5791        source_range,
5792        node_path,
5793        command,
5794        defined_names_stack: Default::default(),
5795    };
5796    let control = dfs_mut(ast, &mut context);
5797    match control {
5798        ControlFlow::Continue(_) => Err(KclError::refactor(
5799            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5800        )),
5801        ControlFlow::Break(break_value) => break_value,
5802    }
5803}
5804
5805#[derive(Debug)]
5806struct AstMutateContext {
5807    source_range: SourceRange,
5808    node_path: Option<ast::NodePath>,
5809    command: AstMutateCommand,
5810    defined_names_stack: Vec<HashSet<String>>,
5811}
5812
5813#[derive(Debug)]
5814#[allow(clippy::large_enum_variant)]
5815enum AstMutateCommand {
5816    /// Add an expression statement to the sketch block.
5817    AddSketchBlockExprStmt {
5818        expr: ast::Expr,
5819    },
5820    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5821    AddSketchBlockVarDecl {
5822        prefix: String,
5823        expr: ast::Expr,
5824    },
5825    AddVariableDeclaration {
5826        prefix: String,
5827    },
5828    EditPoint {
5829        at: ast::Expr,
5830    },
5831    EditLine {
5832        start: ast::Expr,
5833        end: ast::Expr,
5834        construction: Option<bool>,
5835    },
5836    EditArc {
5837        start: ast::Expr,
5838        end: ast::Expr,
5839        center: ast::Expr,
5840        direction: Option<ArcDirection>,
5841        construction: Option<bool>,
5842    },
5843    EditCircle {
5844        start: ast::Expr,
5845        center: ast::Expr,
5846        construction: Option<bool>,
5847    },
5848    EditControlPointSpline {
5849        points: ast::Expr,
5850        construction: Option<bool>,
5851    },
5852    EditConstraintValue {
5853        value: ast::BinaryPart,
5854    },
5855    EditAngleConstraint {
5856        call: ast::BinaryPart,
5857        value: ast::BinaryPart,
5858    },
5859    EditDistanceConstraint {
5860        call: ast::BinaryPart,
5861        value: ast::BinaryPart,
5862    },
5863    EditDistanceConstraintLabelPosition {
5864        label_position: ast::Expr,
5865    },
5866    EditCallUnlabeled {
5867        arg: ast::Expr,
5868    },
5869    EditVarInitialValue {
5870        value: Number,
5871    },
5872    DeleteNode,
5873}
5874
5875impl AstMutateCommand {
5876    fn needs_defined_names_stack(&self) -> bool {
5877        matches!(
5878            self,
5879            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5880        )
5881    }
5882}
5883
5884#[derive(Debug)]
5885enum AstMutateCommandReturn {
5886    None,
5887    Name(String),
5888}
5889
5890#[derive(Debug, Clone)]
5891struct AstNodeRef {
5892    range: SourceRange,
5893    node_path: Option<ast::NodePath>,
5894}
5895
5896impl<T> From<&ast::Node<T>> for AstNodeRef {
5897    fn from(value: &ast::Node<T>) -> Self {
5898        AstNodeRef {
5899            range: value.into(),
5900            node_path: value.node_path.clone(),
5901        }
5902    }
5903}
5904
5905impl From<&ast::BodyItem> for AstNodeRef {
5906    fn from(value: &ast::BodyItem) -> Self {
5907        match value {
5908            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5909                range: node.into(),
5910                node_path: node.node_path.clone(),
5911            },
5912            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5913                range: node.into(),
5914                node_path: node.node_path.clone(),
5915            },
5916            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5917                range: node.into(),
5918                node_path: node.node_path.clone(),
5919            },
5920            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5921                range: node.into(),
5922                node_path: node.node_path.clone(),
5923            },
5924            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5925                range: node.into(),
5926                node_path: node.node_path.clone(),
5927            },
5928        }
5929    }
5930}
5931
5932impl From<&ast::Expr> for AstNodeRef {
5933    fn from(value: &ast::Expr) -> Self {
5934        AstNodeRef {
5935            range: SourceRange::from(value),
5936            node_path: value.node_path().cloned(),
5937        }
5938    }
5939}
5940
5941impl From<&AstMutateContext> for AstNodeRef {
5942    fn from(value: &AstMutateContext) -> Self {
5943        AstNodeRef {
5944            range: value.source_range,
5945            node_path: value.node_path.clone(),
5946        }
5947    }
5948}
5949
5950impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5951    type Error = crate::walk::AstNodeError;
5952
5953    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5954        Ok(AstNodeRef {
5955            range: SourceRange::try_from(value)?,
5956            node_path: value.try_into()?,
5957        })
5958    }
5959}
5960
5961impl From<AstNodeRef> for SourceRange {
5962    fn from(value: AstNodeRef) -> Self {
5963        value.range
5964    }
5965}
5966
5967impl Visitor for AstMutateContext {
5968    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5969    type Continue = ();
5970
5971    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5972        filter_and_process(self, node)
5973    }
5974
5975    fn finish(&mut self, node: NodeMut<'_>) {
5976        match &node {
5977            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5978                self.defined_names_stack.pop();
5979            }
5980            _ => {}
5981        }
5982    }
5983}
5984
5985fn filter_and_process(
5986    ctx: &mut AstMutateContext,
5987    node: NodeMut,
5988) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5989    let Ok(node_range) = SourceRange::try_from(&node) else {
5990        // Nodes that can't be converted to a range aren't interesting.
5991        return TraversalReturn::new_continue(());
5992    };
5993    // If we're adding a variable declaration, we need to look at variable
5994    // declaration expressions to see if it already has a variable, before
5995    // continuing. The variable declaration's source range won't match the
5996    // target; its init expression will.
5997    if let NodeMut::VariableDeclaration(var_decl) = &node {
5998        let expr_range = SourceRange::from(&var_decl.declaration.init);
5999        let expr_node_path = var_decl.declaration.init.node_path();
6000        if source_ref_matches(ctx, expr_range, expr_node_path) {
6001            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6002                // We found the variable declaration expression. It doesn't need
6003                // to be added.
6004                return TraversalReturn::new_break(Ok((
6005                    AstNodeRef::from(&**var_decl),
6006                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6007                )));
6008            }
6009            if let AstMutateCommand::DeleteNode = &ctx.command {
6010                // We found the variable declaration. Delete the variable along
6011                // with the segment.
6012                return TraversalReturn {
6013                    mutate_body_item: MutateBodyItem::Delete,
6014                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6015                };
6016            }
6017        }
6018    }
6019    // Similar thing with expression statement. We need to look at the
6020    // expression inside it.
6021    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6022        let expr_range = SourceRange::from(&expr_stmt.expression);
6023        let expr_node_path = expr_stmt.expression.node_path();
6024        if source_ref_matches(ctx, expr_range, expr_node_path) {
6025            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6026                // We found the node wrapped in an expression statement. Process
6027                // the statement.
6028                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6029                    return TraversalReturn::new_continue(());
6030                };
6031                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6032            }
6033            if let AstMutateCommand::DeleteNode = &ctx.command {
6034                // We found the node wrapped in an expression statement. Delete
6035                // the whole statement.
6036                return TraversalReturn {
6037                    mutate_body_item: MutateBodyItem::Delete,
6038                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6039                };
6040            }
6041        }
6042    }
6043
6044    if ctx.command.needs_defined_names_stack() {
6045        if let NodeMut::Program(program) = &node {
6046            ctx.defined_names_stack.push(find_defined_names(*program));
6047        } else if let NodeMut::SketchBlock(block) = &node {
6048            ctx.defined_names_stack.push(find_defined_names(&block.body));
6049        }
6050    }
6051
6052    // Make sure the node matches the source ref.
6053    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6054    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6055        return TraversalReturn::new_continue(());
6056    }
6057    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6058        return TraversalReturn::new_continue(());
6059    };
6060    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6061}
6062
6063fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6064    match &ctx.node_path {
6065        Some(target) => Some(target) == node_path,
6066        None => node_range == ctx.source_range,
6067    }
6068}
6069
6070fn is_angle_constraint_call_name(name: &str) -> bool {
6071    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6072}
6073
6074fn is_distance_constraint_call_name(name: &str) -> bool {
6075    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6076}
6077
6078fn is_constraint_call_name(name: &str) -> bool {
6079    matches!(
6080        name,
6081        DISTANCE_FN
6082            | HORIZONTAL_DISTANCE_FN
6083            | VERTICAL_DISTANCE_FN
6084            | RADIUS_FN
6085            | DIAMETER_FN
6086            | ANGLE_FN
6087            | ANGLE_DIMENSION_FN
6088    )
6089}
6090
6091fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6092    if let ast::BinaryPart::CallExpressionKw(call) = part
6093        && is_constraint_call_name(call.callee.name.name.as_str())
6094    {
6095        Some(call)
6096    } else {
6097        None
6098    }
6099}
6100
6101fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6102    match &ctx.command {
6103        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6104            if let NodeMut::SketchBlock(sketch_block) = node {
6105                sketch_block
6106                    .body
6107                    .items
6108                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6109                        inner: ast::ExpressionStatement {
6110                            expression: expr.clone(),
6111                            digest: None,
6112                        },
6113                        start: Default::default(),
6114                        end: Default::default(),
6115                        module_id: Default::default(),
6116                        node_path: None,
6117                        outer_attrs: Default::default(),
6118                        pre_comments: Default::default(),
6119                        comment_start: Default::default(),
6120                    }));
6121                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6122            }
6123        }
6124        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6125            if let NodeMut::SketchBlock(sketch_block) = node {
6126                let empty_defined_names = HashSet::new();
6127                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6128                let Ok(name) = next_free_name(prefix, defined_names) else {
6129                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6130                };
6131                sketch_block
6132                    .body
6133                    .items
6134                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6135                        ast::VariableDeclaration::new(
6136                            ast::VariableDeclarator::new(&name, expr.clone()),
6137                            ast::ItemVisibility::Default,
6138                            ast::VariableKind::Const,
6139                        ),
6140                    ))));
6141                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6142            }
6143        }
6144        AstMutateCommand::AddVariableDeclaration { prefix } => {
6145            if let NodeMut::VariableDeclaration(inner) = node {
6146                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6147            }
6148            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6149                let empty_defined_names = HashSet::new();
6150                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6151                let Ok(name) = next_free_name(prefix, defined_names) else {
6152                    // TODO: Return an error instead?
6153                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6154                };
6155                let mutate_node =
6156                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6157                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6158                        ast::ItemVisibility::Default,
6159                        ast::VariableKind::Const,
6160                    ))));
6161                return TraversalReturn {
6162                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6163                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6164                };
6165            }
6166        }
6167        AstMutateCommand::EditPoint { at } => {
6168            if let NodeMut::CallExpressionKw(call) = node {
6169                if call.callee.name.name != POINT_FN {
6170                    return TraversalReturn::new_continue(());
6171                }
6172                // Update the arguments.
6173                for labeled_arg in &mut call.arguments {
6174                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6175                        labeled_arg.arg = at.clone();
6176                    }
6177                }
6178                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6179            }
6180        }
6181        AstMutateCommand::EditLine {
6182            start,
6183            end,
6184            construction,
6185        } => {
6186            if let NodeMut::CallExpressionKw(call) = node {
6187                if call.callee.name.name != LINE_FN {
6188                    return TraversalReturn::new_continue(());
6189                }
6190                // Update the arguments.
6191                for labeled_arg in &mut call.arguments {
6192                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6193                        labeled_arg.arg = start.clone();
6194                    }
6195                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6196                        labeled_arg.arg = end.clone();
6197                    }
6198                }
6199                // Handle construction kwarg
6200                if let Some(construction_value) = construction {
6201                    let construction_exists = call
6202                        .arguments
6203                        .iter()
6204                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6205                    if *construction_value {
6206                        // Add or update construction=true
6207                        if construction_exists {
6208                            // Update existing construction kwarg
6209                            for labeled_arg in &mut call.arguments {
6210                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6211                                    labeled_arg.arg =
6212                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6213                                            value: ast::LiteralValue::Bool(true),
6214                                            raw: "true".to_string(),
6215                                            digest: None,
6216                                        })));
6217                                }
6218                            }
6219                        } else {
6220                            // Add new construction kwarg
6221                            call.arguments.push(ast::LabeledArg {
6222                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6223                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6224                                    value: ast::LiteralValue::Bool(true),
6225                                    raw: "true".to_string(),
6226                                    digest: None,
6227                                }))),
6228                            });
6229                        }
6230                    } else {
6231                        // Remove construction kwarg if it exists
6232                        call.arguments
6233                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6234                    }
6235                }
6236                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6237            }
6238        }
6239        AstMutateCommand::EditArc {
6240            start,
6241            end,
6242            center,
6243            direction,
6244            construction,
6245        } => {
6246            if let NodeMut::CallExpressionKw(call) = node {
6247                if call.callee.name.name != ARC_FN {
6248                    return TraversalReturn::new_continue(());
6249                }
6250                // Update the arguments.
6251                for labeled_arg in &mut call.arguments {
6252                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6253                        labeled_arg.arg = start.clone();
6254                    }
6255                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6256                        labeled_arg.arg = end.clone();
6257                    }
6258                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6259                        labeled_arg.arg = center.clone();
6260                    }
6261                }
6262                // Handle direction kwarg
6263                if let Some(direction_value) = direction {
6264                    let direction_exists = call
6265                        .arguments
6266                        .iter()
6267                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6268                    if direction_value.is_clockwise() {
6269                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6270                        if direction_exists {
6271                            // Update existing direction kwarg
6272                            for labeled_arg in &mut call.arguments {
6273                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6274                                    labeled_arg.arg = direction_ast.clone();
6275                                }
6276                            }
6277                        } else {
6278                            // Add new direction kwarg
6279                            call.arguments.push(ast::LabeledArg {
6280                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6281                                arg: direction_ast,
6282                            });
6283                        }
6284                    } else {
6285                        // Remove direction kwarg if it exists since
6286                        // counterclockwise is the default
6287                        call.arguments
6288                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6289                    }
6290                }
6291                // Handle construction kwarg
6292                if let Some(construction_value) = construction {
6293                    let construction_exists = call
6294                        .arguments
6295                        .iter()
6296                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6297                    if *construction_value {
6298                        // Add or update construction=true
6299                        if construction_exists {
6300                            // Update existing construction kwarg
6301                            for labeled_arg in &mut call.arguments {
6302                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6303                                    labeled_arg.arg =
6304                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6305                                            value: ast::LiteralValue::Bool(true),
6306                                            raw: "true".to_string(),
6307                                            digest: None,
6308                                        })));
6309                                }
6310                            }
6311                        } else {
6312                            // Add new construction kwarg
6313                            call.arguments.push(ast::LabeledArg {
6314                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6315                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6316                                    value: ast::LiteralValue::Bool(true),
6317                                    raw: "true".to_string(),
6318                                    digest: None,
6319                                }))),
6320                            });
6321                        }
6322                    } else {
6323                        // Remove construction kwarg if it exists
6324                        call.arguments
6325                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6326                    }
6327                }
6328                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6329            }
6330        }
6331        AstMutateCommand::EditCircle {
6332            start,
6333            center,
6334            construction,
6335        } => {
6336            if let NodeMut::CallExpressionKw(call) = node {
6337                if call.callee.name.name != CIRCLE_FN {
6338                    return TraversalReturn::new_continue(());
6339                }
6340                // Update the arguments.
6341                for labeled_arg in &mut call.arguments {
6342                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6343                        labeled_arg.arg = start.clone();
6344                    }
6345                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6346                        labeled_arg.arg = center.clone();
6347                    }
6348                }
6349                // Handle construction kwarg
6350                if let Some(construction_value) = construction {
6351                    let construction_exists = call
6352                        .arguments
6353                        .iter()
6354                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6355                    if *construction_value {
6356                        if construction_exists {
6357                            // Update existing construction kwarg
6358                            for labeled_arg in &mut call.arguments {
6359                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6360                                    labeled_arg.arg =
6361                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6362                                            value: ast::LiteralValue::Bool(true),
6363                                            raw: "true".to_string(),
6364                                            digest: None,
6365                                        })));
6366                                }
6367                            }
6368                        } else {
6369                            // Add new construction kwarg
6370                            call.arguments.push(ast::LabeledArg {
6371                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6372                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6373                                    value: ast::LiteralValue::Bool(true),
6374                                    raw: "true".to_string(),
6375                                    digest: None,
6376                                }))),
6377                            });
6378                        }
6379                    } else {
6380                        // Remove construction kwarg if it exists
6381                        call.arguments
6382                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6383                    }
6384                }
6385                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6386            }
6387        }
6388        AstMutateCommand::EditControlPointSpline { points, construction } => {
6389            if let NodeMut::CallExpressionKw(call) = node {
6390                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6391                    return TraversalReturn::new_continue(());
6392                }
6393                for labeled_arg in &mut call.arguments {
6394                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6395                    {
6396                        labeled_arg.arg = points.clone();
6397                    }
6398                }
6399                // Handle construction kwarg
6400                if let Some(construction_value) = construction {
6401                    let construction_exists = call
6402                        .arguments
6403                        .iter()
6404                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6405                    if *construction_value {
6406                        if construction_exists {
6407                            for labeled_arg in &mut call.arguments {
6408                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6409                                    labeled_arg.arg =
6410                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6411                                            value: ast::LiteralValue::Bool(true),
6412                                            raw: "true".to_string(),
6413                                            digest: None,
6414                                        })));
6415                                }
6416                            }
6417                        } else {
6418                            call.arguments.push(ast::LabeledArg {
6419                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6420                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421                                    value: ast::LiteralValue::Bool(true),
6422                                    raw: "true".to_string(),
6423                                    digest: None,
6424                                }))),
6425                            });
6426                        }
6427                    } else {
6428                        call.arguments
6429                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6430                    }
6431                }
6432                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6433            }
6434        }
6435        AstMutateCommand::EditConstraintValue { value } => {
6436            if let NodeMut::BinaryExpression(binary_expr) = node {
6437                let left_is_constraint = matches!(
6438                    &binary_expr.left,
6439                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6440                );
6441                if left_is_constraint {
6442                    binary_expr.right = value.clone();
6443                } else {
6444                    binary_expr.left = value.clone();
6445                }
6446
6447                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6448            }
6449        }
6450        AstMutateCommand::EditAngleConstraint { call, value } => {
6451            if let NodeMut::BinaryExpression(binary_expr) = node {
6452                let left_is_angle = matches!(
6453                    &binary_expr.left,
6454                    ast::BinaryPart::CallExpressionKw(existing_call)
6455                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6456                );
6457                let right_is_angle = matches!(
6458                    &binary_expr.right,
6459                    ast::BinaryPart::CallExpressionKw(existing_call)
6460                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6461                );
6462
6463                match (left_is_angle, right_is_angle) {
6464                    (true, _) => {
6465                        binary_expr.left = call.clone();
6466                        binary_expr.right = value.clone();
6467                    }
6468                    (false, true) => {
6469                        binary_expr.left = value.clone();
6470                        binary_expr.right = call.clone();
6471                    }
6472                    (false, false) => return TraversalReturn::new_continue(()),
6473                }
6474
6475                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6476            }
6477        }
6478        AstMutateCommand::EditDistanceConstraint { call, value } => {
6479            if let NodeMut::BinaryExpression(binary_expr) = node {
6480                let left_is_distance = matches!(
6481                    &binary_expr.left,
6482                    ast::BinaryPart::CallExpressionKw(existing_call)
6483                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6484                );
6485                let right_is_distance = matches!(
6486                    &binary_expr.right,
6487                    ast::BinaryPart::CallExpressionKw(existing_call)
6488                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6489                );
6490
6491                match (left_is_distance, right_is_distance) {
6492                    (true, _) => {
6493                        binary_expr.left = call.clone();
6494                        binary_expr.right = value.clone();
6495                    }
6496                    (false, true) => {
6497                        binary_expr.left = value.clone();
6498                        binary_expr.right = call.clone();
6499                    }
6500                    (false, false) => return TraversalReturn::new_continue(()),
6501                }
6502
6503                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6504            }
6505        }
6506        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6507            if let NodeMut::BinaryExpression(binary_expr) = node {
6508                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6509                    call
6510                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6511                    call
6512                } else {
6513                    return TraversalReturn::new_continue(());
6514                };
6515
6516                if let Some(label_arg) = call
6517                    .arguments
6518                    .iter_mut()
6519                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6520                {
6521                    label_arg.arg = label_position.clone();
6522                } else {
6523                    call.arguments.push(ast::LabeledArg {
6524                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6525                        arg: label_position.clone(),
6526                    });
6527                }
6528
6529                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6530            }
6531        }
6532        AstMutateCommand::EditCallUnlabeled { arg } => {
6533            if let NodeMut::CallExpressionKw(call) = node {
6534                call.unlabeled = Some(arg.clone());
6535                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536            }
6537        }
6538        AstMutateCommand::EditVarInitialValue { value } => {
6539            // We target the SketchVar itself (matched by NodePath) rather than
6540            // the inner NumericLiteral so we can also write back into vars that
6541            // were declared without an initial value (e.g. bare `var`).
6542            if let NodeMut::SketchVar(sketch_var) = node {
6543                let Ok(literal) = to_source_number(*value) else {
6544                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6545                        "Could not convert number to AST literal: {:?}",
6546                        *value
6547                    ))));
6548                };
6549                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6550                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6551            }
6552        }
6553        AstMutateCommand::DeleteNode => {
6554            return TraversalReturn {
6555                mutate_body_item: MutateBodyItem::Delete,
6556                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6557            };
6558        }
6559    }
6560    TraversalReturn::new_continue(())
6561}
6562
6563struct FindSketchBlockSourceRange {
6564    /// The source range of the sketch block before mutation.
6565    target_before_mutation: SourceRange,
6566    /// The source range of the sketch block's last body item after mutation. We
6567    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6568    /// shared reference.
6569    found: Cell<Option<AstNodeRef>>,
6570}
6571
6572impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6573    type Error = crate::front::Error;
6574
6575    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6576        let Ok(node_range) = SourceRange::try_from(&node) else {
6577            return Ok(true);
6578        };
6579
6580        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6581            if node_range.module_id() == self.target_before_mutation.module_id()
6582                && node_range.start() == self.target_before_mutation.start()
6583                // End shouldn't match since we added something.
6584                && node_range.end() >= self.target_before_mutation.end()
6585            {
6586                self.found.set(sketch_block.body.items.last().map(|item| match item {
6587                    // For declarations like `circle1 = circle(...)`, use
6588                    // the init expression range so lookup in source_range_to_object
6589                    // matches the segment source range.
6590                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6591                    _ => AstNodeRef::from(item),
6592                }));
6593                return Ok(false);
6594            } else {
6595                // We found a different sketch block. No need to descend into
6596                // its children since sketch blocks cannot be nested.
6597                return Ok(true);
6598            }
6599        }
6600
6601        for child in node.children().iter() {
6602            if !child.visit(*self)? {
6603                return Ok(false);
6604            }
6605        }
6606
6607        Ok(true)
6608    }
6609}
6610
6611struct FindSketchBlockByNodePath {
6612    /// The Node Path of the sketch block before mutation.
6613    target_node_path: ast::NodePath,
6614    /// The ref of the sketch block's last body item after mutation. We need to
6615    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6616    /// reference.
6617    found: Cell<Option<AstNodeRef>>,
6618}
6619
6620impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6621    type Error = crate::front::Error;
6622
6623    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6624        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6625            return Ok(true);
6626        };
6627
6628        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6629            if let Some(node_path) = node_path
6630                && node_path == self.target_node_path
6631            {
6632                self.found.set(sketch_block.body.items.last().map(|item| match item {
6633                    // For declarations like `circle1 = circle(...)`, use
6634                    // the init expression range so lookup in source_range_to_object
6635                    // matches the segment source range.
6636                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6637                    _ => AstNodeRef::from(item),
6638                }));
6639
6640                return Ok(false);
6641            } else {
6642                // We found a different sketch block. No need to descend into
6643                // its children since sketch blocks cannot be nested.
6644                return Ok(true);
6645            }
6646        }
6647
6648        for child in node.children().iter() {
6649            if !child.visit(*self)? {
6650                return Ok(false);
6651            }
6652        }
6653
6654        Ok(true)
6655    }
6656}
6657
6658/// After adding an item to a sketch block, find the sketch block, and get the
6659/// source range of the added item. We assume that the added item is the last
6660/// item in the sketch block and that the sketch block's source range has grown,
6661/// but not moved from its starting offset.
6662///
6663/// TODO: Do we need to format *before* mutation in case formatting moves the
6664/// sketch block forward?
6665fn find_sketch_block_added_item(
6666    ast: &ast::Node<ast::Program>,
6667    sketch_block_before_mutation: &AstNodeRef,
6668) -> Result<AstNodeRef, KclError> {
6669    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6670        let find = FindSketchBlockByNodePath {
6671            target_node_path: node_path.clone(),
6672            found: Cell::new(None),
6673        };
6674        let node = crate::walk::Node::from(ast);
6675        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6676        find.found.into_inner().ok_or_else(|| {
6677            KclError::refactor(format!(
6678                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6679            ))
6680        })
6681    } else {
6682        // No NodePath. Fall back to legacy source range.
6683        let find = FindSketchBlockSourceRange {
6684            target_before_mutation: sketch_block_before_mutation.range,
6685            found: Cell::new(None),
6686        };
6687        let node = crate::walk::Node::from(ast);
6688        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689        find.found.into_inner().ok_or_else(|| KclError::refactor(
6690            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?"),
6691        ))
6692    }
6693}
6694
6695fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6696    let message = error.message().trim();
6697    let message = if message.is_empty() {
6698        "unknown parse error"
6699    } else {
6700        message
6701    };
6702
6703    format!("{prefix}: {message}")
6704}
6705
6706fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6707    let (program, errors) = Program::parse(source).map_err(|err| {
6708        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6709    })?;
6710    if !errors.is_empty() {
6711        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6712            format_compilation_issues(parse_error_prefix, &errors),
6713        )));
6714    }
6715
6716    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6717}
6718
6719fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6720    let Some(first_issue) = issues
6721        .iter()
6722        .find(|issue| issue.severity.is_err())
6723        .or_else(|| issues.first())
6724    else {
6725        return prefix.to_owned();
6726    };
6727
6728    let message = first_issue.message.trim();
6729    let message = if message.is_empty() {
6730        "unknown parse error"
6731    } else {
6732        message
6733    };
6734
6735    if issues.len() > 1 {
6736        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6737    } else {
6738        format!("{prefix}: {message}")
6739    }
6740}
6741
6742fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6743    // TODO: Don't duplicate this from lib.rs Program.
6744    ast.recast_top(&Default::default(), 0)
6745}
6746
6747struct FindNumericLiteral {
6748    target: SourceRange,
6749    found: Cell<Option<ast::NumericLiteral>>,
6750}
6751
6752impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6753    type Error = crate::front::Error;
6754
6755    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6756        let Ok(node_range) = SourceRange::try_from(&node) else {
6757            return Ok(true);
6758        };
6759
6760        if node_range == self.target
6761            && let crate::walk::Node::NumericLiteral(literal) = node
6762        {
6763            self.found.set(Some(literal.inner.clone()));
6764            return Ok(false);
6765        }
6766
6767        for child in node.children().iter() {
6768            if !child.visit(*self)? {
6769                return Ok(false);
6770            }
6771        }
6772
6773        Ok(true)
6774    }
6775}
6776
6777fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6778    let find = FindNumericLiteral {
6779        target,
6780        found: Cell::new(None),
6781    };
6782    let node = crate::walk::Node::from(ast);
6783    node.visit(&find).ok()?;
6784    find.found.into_inner()
6785}
6786
6787struct FindSketchVarInitialByNodePath<'a> {
6788    target: &'a ast::NodePath,
6789    sketch_var_found: Cell<bool>,
6790    initial_literal: Cell<Option<ast::NumericLiteral>>,
6791}
6792
6793impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6794    type Error = crate::front::Error;
6795
6796    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6797        if let crate::walk::Node::SketchVar(sketch_var) = node
6798            && sketch_var.node_path.as_ref() == Some(self.target)
6799        {
6800            self.sketch_var_found.set(true);
6801            if let Some(initial) = &sketch_var.initial {
6802                self.initial_literal.set(Some(initial.inner.clone()));
6803            }
6804            return Ok(false);
6805        }
6806
6807        for child in node.children().iter() {
6808            if !child.visit(*self)? {
6809                return Ok(false);
6810            }
6811        }
6812
6813        Ok(true)
6814    }
6815}
6816
6817/// Locate the source `var` declaration corresponding to a sketch-var solution.
6818///
6819/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6820/// from "target found." The inner [`Option`] is the initial numeric literal of
6821/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6822///
6823/// When `node_path` is `None` (e.g. for older outcomes that predate the
6824/// node-path propagation), this falls back to source-range matching, which
6825/// can break under whitespace shifts elsewhere in the file.
6826fn numeric_literal_at_node_path(
6827    ast: &ast::Node<ast::Program>,
6828    node_path: Option<&ast::NodePath>,
6829    source_range: SourceRange,
6830) -> Option<Option<ast::NumericLiteral>> {
6831    let Some(node_path) = node_path else {
6832        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";
6833        #[cfg(target_arch = "wasm32")]
6834        web_sys::console::warn_1(&message.into());
6835        #[cfg(not(target_arch = "wasm32"))]
6836        eprintln!("WARNING: {message}");
6837        return numeric_literal_at_source_range(ast, source_range).map(Some);
6838    };
6839    let find = FindSketchVarInitialByNodePath {
6840        target: node_path,
6841        sketch_var_found: Cell::new(false),
6842        initial_literal: Cell::new(None),
6843    };
6844    let node = crate::walk::Node::from(ast);
6845    node.visit(&find).ok()?;
6846    if !find.sketch_var_found.get() {
6847        return None;
6848    }
6849    Some(find.initial_literal.into_inner())
6850}
6851
6852fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6853    match suffix {
6854        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6855        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6856        NumericSuffix::M => Some(UnitLength::Meters),
6857        NumericSuffix::Inch => Some(UnitLength::Inches),
6858        NumericSuffix::Ft => Some(UnitLength::Feet),
6859        NumericSuffix::Yd => Some(UnitLength::Yards),
6860        _ => None,
6861    }
6862}
6863
6864fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6865    match suffix_length_unit(number.units) {
6866        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6867        None => number.value,
6868    }
6869}
6870
6871fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6872    match suffix_length_unit(literal.suffix) {
6873        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6874        None => literal.value,
6875    }
6876}
6877
6878fn var_solution_needs_commit(
6879    current_literal: &ast::NumericLiteral,
6880    solved_value: Number,
6881    default_length_unit: UnitLength,
6882) -> bool {
6883    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6884    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6885
6886    (current - solved).abs() > 1e-9
6887}
6888
6889fn preserve_var_solution_literal_style(
6890    current_literal: &ast::NumericLiteral,
6891    solved_value: Number,
6892    default_length_unit: UnitLength,
6893) -> Number {
6894    if current_literal.suffix == NumericSuffix::None {
6895        return Number {
6896            value: number_value_in_default_length_units(solved_value, default_length_unit),
6897            units: NumericSuffix::None,
6898        };
6899    }
6900
6901    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6902        return solved_value;
6903    };
6904
6905    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6906    Number {
6907        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6908        units: current_literal.suffix,
6909    }
6910}
6911
6912pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6913    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6914        inner: ast::ArrayExpression {
6915            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6916            non_code_meta: Default::default(),
6917            digest: None,
6918        },
6919        start: Default::default(),
6920        end: Default::default(),
6921        module_id: Default::default(),
6922        node_path: None,
6923        outer_attrs: Default::default(),
6924        pre_comments: Default::default(),
6925        comment_start: Default::default(),
6926    })))
6927}
6928
6929pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6930    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6931        ast::ArrayExpression {
6932            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6933            digest: None,
6934            non_code_meta: Default::default(),
6935        },
6936    ))))
6937}
6938
6939fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6940    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941        ast::ArrayExpression {
6942            elements: vec![
6943                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6944                    point.x,
6945                )?)))),
6946                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6947                    point.y,
6948                )?)))),
6949            ],
6950            non_code_meta: Default::default(),
6951            digest: None,
6952        },
6953    ))))
6954}
6955
6956fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6957    match expr {
6958        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6959            inner: ast::Literal::from(to_source_number(*number)?),
6960            start: Default::default(),
6961            end: Default::default(),
6962            module_id: Default::default(),
6963            node_path: None,
6964            outer_attrs: Default::default(),
6965            pre_comments: Default::default(),
6966            comment_start: Default::default(),
6967        }))),
6968        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6969            inner: ast::SketchVar {
6970                initial: Some(BoxNode::new(ast::Node {
6971                    inner: to_source_number(*number)?,
6972                    start: Default::default(),
6973                    end: Default::default(),
6974                    module_id: Default::default(),
6975                    node_path: None,
6976                    outer_attrs: Default::default(),
6977                    pre_comments: Default::default(),
6978                    comment_start: Default::default(),
6979                })),
6980                digest: None,
6981            },
6982            start: Default::default(),
6983            end: Default::default(),
6984            module_id: Default::default(),
6985            node_path: None,
6986            outer_attrs: Default::default(),
6987            pre_comments: Default::default(),
6988            comment_start: Default::default(),
6989        }))),
6990        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6991    }
6992}
6993
6994fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6995    Ok(ast::NumericLiteral {
6996        value: number.value,
6997        suffix: number.units,
6998        raw: format_number_literal(number.value, number.units, None)?,
6999        digest: None,
7000    })
7001}
7002
7003pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7004    ast::Expr::Name(BoxNode::new(ast_name(name)))
7005}
7006
7007fn ast_name(name: String) -> ast::Node<ast::Name> {
7008    ast::Node {
7009        inner: ast::Name {
7010            name: ast::Node {
7011                inner: ast::Identifier { name, digest: None },
7012                start: Default::default(),
7013                end: Default::default(),
7014                module_id: Default::default(),
7015                node_path: None,
7016                outer_attrs: Default::default(),
7017                pre_comments: Default::default(),
7018                comment_start: Default::default(),
7019            },
7020            path: Vec::new(),
7021            abs_path: false,
7022            digest: None,
7023        },
7024        start: Default::default(),
7025        end: Default::default(),
7026        module_id: Default::default(),
7027        node_path: None,
7028        outer_attrs: Default::default(),
7029        pre_comments: Default::default(),
7030        comment_start: Default::default(),
7031    }
7032}
7033
7034pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7035    ast::Name {
7036        name: ast::Node {
7037            inner: ast::Identifier {
7038                name: name.to_owned(),
7039                digest: None,
7040            },
7041            start: Default::default(),
7042            end: Default::default(),
7043            module_id: Default::default(),
7044            node_path: None,
7045            outer_attrs: Default::default(),
7046            pre_comments: Default::default(),
7047            comment_start: Default::default(),
7048        },
7049        path: Default::default(),
7050        abs_path: false,
7051        digest: None,
7052    }
7053}
7054
7055/// Create an AST node for coincident([expr1, expr2, ...])
7056pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7057    let elements = exprs.into_iter().collect::<Vec<_>>();
7058    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7059
7060    // Create array [expr1, expr2, ...]
7061    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7062        elements,
7063        digest: None,
7064        non_code_meta: Default::default(),
7065    })));
7066
7067    // Create coincident([...])
7068    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7069        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7070        unlabeled: Some(array_expr),
7071        arguments: Default::default(),
7072        digest: None,
7073        non_code_meta: Default::default(),
7074    })))
7075}
7076
7077/// Create an AST node for horizontal(line)
7078pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7079    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7080        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7081        unlabeled: Some(line_expr),
7082        arguments: Default::default(),
7083        digest: None,
7084        non_code_meta: Default::default(),
7085    })))
7086}
7087
7088/// Create an AST node for vertical(line)
7089pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7090    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7092        unlabeled: Some(line_expr),
7093        arguments: Default::default(),
7094        digest: None,
7095        non_code_meta: Default::default(),
7096    })))
7097}
7098
7099/// Create a member expression like object.property (e.g., line1.end)
7100pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7101    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7102        object: object_expr,
7103        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7104            name: ast::Node::no_src(ast::Identifier {
7105                name: property.to_string(),
7106                digest: None,
7107            }),
7108            path: Vec::new(),
7109            abs_path: false,
7110            digest: None,
7111        }))),
7112        computed: false,
7113        digest: None,
7114    })))
7115}
7116
7117pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7118    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7119        object: object_expr,
7120        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7121            ast::NumericLiteral {
7122                value: index as f64,
7123                suffix: NumericSuffix::None,
7124                raw: index.to_string(),
7125                digest: None,
7126            },
7127        )))),
7128        computed: true,
7129        digest: None,
7130    })))
7131}
7132
7133/// Create an AST node for `fixed([point, [x, y]])`.
7134fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7135    // Create [x, y] array literal.
7136    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7137        position.x,
7138    )?))));
7139    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7140        position.y,
7141    )?))));
7142    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7143        elements: vec![x_literal, y_literal],
7144        digest: None,
7145        non_code_meta: Default::default(),
7146    })));
7147
7148    // Create [point, [x, y]] outer array.
7149    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7150        elements: vec![point_expr, point_array],
7151        digest: None,
7152        non_code_meta: Default::default(),
7153    })));
7154
7155    // Create fixed([...])
7156    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7157        ast::CallExpressionKw {
7158            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7159            unlabeled: Some(array_expr),
7160            arguments: Default::default(),
7161            digest: None,
7162            non_code_meta: Default::default(),
7163        },
7164    ))))
7165}
7166
7167/// Create an AST node for equalLength([line1, line2, ...])
7168pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7169    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7170        elements: line_exprs,
7171        digest: None,
7172        non_code_meta: Default::default(),
7173    })));
7174
7175    // Create equalLength([...])
7176    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7177        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7178        unlabeled: Some(array_expr),
7179        arguments: Default::default(),
7180        digest: None,
7181        non_code_meta: Default::default(),
7182    })))
7183}
7184
7185/// Create an AST node for equalRadius([seg1, seg2, ...])
7186pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7187    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7188        elements: segment_exprs,
7189        digest: None,
7190        non_code_meta: Default::default(),
7191    })));
7192
7193    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7194        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7195        unlabeled: Some(array_expr),
7196        arguments: Default::default(),
7197        digest: None,
7198        non_code_meta: Default::default(),
7199    })))
7200}
7201
7202/// Create an AST node for tangent([seg1, seg2])
7203pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7204    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205        elements: vec![seg1_expr, seg2_expr],
7206        digest: None,
7207        non_code_meta: Default::default(),
7208    })));
7209
7210    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7211        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7212        unlabeled: Some(array_expr),
7213        arguments: Default::default(),
7214        digest: None,
7215        non_code_meta: Default::default(),
7216    })))
7217}
7218
7219/// Create an AST node for symmetric([input1, input2], axis = line)
7220pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7221    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7222        elements: input_exprs,
7223        digest: None,
7224        non_code_meta: Default::default(),
7225    })));
7226    let arguments = vec![ast::LabeledArg {
7227        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7228        arg: axis_expr,
7229    }];
7230
7231    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7232        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7233        unlabeled: Some(array_expr),
7234        arguments,
7235        digest: None,
7236        non_code_meta: Default::default(),
7237    })))
7238}
7239
7240/// Create an AST node for midpoint(segment, point = point)
7241pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7242    let arguments = vec![ast::LabeledArg {
7243        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7244        arg: point_expr,
7245    }];
7246
7247    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7248        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7249        unlabeled: Some(segment_expr),
7250        arguments,
7251        digest: None,
7252        non_code_meta: Default::default(),
7253    })))
7254}
7255
7256/// The source range to attach to a diagnostic for `error` in the top-level
7257/// module. Source ranges are ordered innermost first and may point into
7258/// imported modules, so prefer the innermost range that is in the top-level
7259/// module; otherwise fall back to the innermost range.
7260fn issue_source_range(error: &KclError) -> SourceRange {
7261    let source_ranges = error.source_ranges();
7262    source_ranges
7263        .iter()
7264        .find(|range| range.is_top_level_module())
7265        .or_else(|| source_ranges.first())
7266        .copied()
7267        .unwrap_or_else(SourceRange::synthetic)
7268}
7269
7270#[cfg(test)]
7271mod tests {
7272    use std::sync;
7273
7274    use super::*;
7275    use crate::engine::PlaneName;
7276    use crate::engine::engine_manager::EngineManager;
7277    use crate::execution::cache::SketchModeState;
7278    use crate::execution::cache::clear_mem_cache;
7279    use crate::execution::cache::read_old_memory;
7280    use crate::execution::cache::write_old_memory;
7281    use crate::front::Distance;
7282    use crate::front::Fixed;
7283    use crate::front::FixedPoint;
7284    use crate::front::Midpoint;
7285    use crate::front::Object;
7286    use crate::front::Plane;
7287    use crate::front::Sketch;
7288    use crate::front::Tangent;
7289    use crate::frontend::sketch::Vertical;
7290    use crate::pretty::NumericSuffix;
7291
7292    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7293        for object in &scene_graph.objects {
7294            if let ObjectKind::Sketch(_) = &object.kind {
7295                return Some(object);
7296            }
7297        }
7298        None
7299    }
7300
7301    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7302        for object in &scene_graph.objects {
7303            if let ObjectKind::Face(_) = &object.kind {
7304                return Some(object);
7305            }
7306        }
7307        None
7308    }
7309
7310    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7311        for object in &scene_graph.objects {
7312            if matches!(&object.kind, ObjectKind::Wall(_)) {
7313                return Some(object.id);
7314            }
7315        }
7316        None
7317    }
7318
7319    fn find_cap_object_id_with_solid_output_index(
7320        scene_graph: &SceneGraph,
7321        cap_kind: crate::frontend::api::CapKind,
7322        solid_output_index: usize,
7323    ) -> Option<ObjectId> {
7324        for object in &scene_graph.objects {
7325            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7326            {
7327                return Some(object.id);
7328            }
7329        }
7330        None
7331    }
7332
7333    #[test]
7334    fn issue_source_range_prefers_top_level_module() {
7335        use kcl_error::ModuleId;
7336
7337        let top = SourceRange::new(10, 20, ModuleId::default());
7338        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7339
7340        // Innermost frame is in an imported module; the diagnostic should
7341        // land on the innermost top-level range instead.
7342        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7343            "boom".to_owned(),
7344            vec![imported, top],
7345        ));
7346        assert_eq!(super::issue_source_range(&error), top);
7347
7348        // No top-level range at all: fall back to the innermost one.
7349        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7350        assert_eq!(super::issue_source_range(&error), imported);
7351
7352        // No ranges: synthetic.
7353        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7354        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7355    }
7356
7357    #[test]
7358    fn composite_constituent_sweeps_are_not_solid_outputs() {
7359        use kcl_api::artifact::ArtifactSweepMethod;
7360        use kcl_api::artifact::CompositeSolid;
7361        use kcl_api::artifact::CompositeSolidSubType;
7362        use kcl_api::artifact::Sweep;
7363        use kcl_api::artifact::SweepSubType;
7364
7365        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7366        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7367        let composite_id = ArtifactId::new(Uuid::new_v4());
7368        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7369        let sweep = |id| {
7370            Artifact::Sweep(Sweep {
7371                id,
7372                sub_type: SweepSubType::Extrusion,
7373                path_id: ArtifactId::new(Uuid::new_v4()),
7374                surface_ids: Vec::new(),
7375                edge_ids: Vec::new(),
7376                code_ref: code_ref.clone(),
7377                source_sweep_id: None,
7378                trajectory_id: None,
7379                method: ArtifactSweepMethod::New,
7380                consumed: false,
7381                pattern_ids: Vec::new(),
7382            })
7383        };
7384        let mut artifacts = IndexMap::from([
7385            (first_sweep_id, sweep(first_sweep_id)),
7386            (second_sweep_id, sweep(second_sweep_id)),
7387        ]);
7388
7389        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7390        assert_eq!(
7391            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7392            Some(0)
7393        );
7394        assert_eq!(
7395            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7396            Some(1)
7397        );
7398
7399        artifacts.insert(
7400            composite_id,
7401            Artifact::CompositeSolid(CompositeSolid {
7402                id: composite_id,
7403                consumed: false,
7404                sub_type: CompositeSolidSubType::Union,
7405                output_index: None,
7406                solid_ids: vec![first_sweep_id, second_sweep_id],
7407                tool_ids: Vec::new(),
7408                code_ref,
7409                composite_solid_id: None,
7410                pattern_ids: Vec::new(),
7411            }),
7412        );
7413        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7414        assert_eq!(
7415            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7416            None
7417        );
7418        assert_eq!(
7419            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7420            None
7421        );
7422    }
7423
7424    #[test]
7425    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7426        let source = "\
7427region001 = region(point = [0.1, 0.1], sketch = s)
7428extrude001 = extrude(region001, length = 5)
7429revolve001 = revolve(region001, axis = Y)
7430sweep001 = sweep(region001, path = path001)
7431loft001 = loft(region001)
7432not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7433";
7434
7435        let program = Program::parse(source).unwrap().0.unwrap();
7436
7437        assert_eq!(
7438            region_name_from_sweep_variable(&program.ast, "extrude001"),
7439            Some("region001".to_owned())
7440        );
7441        assert_eq!(
7442            region_name_from_sweep_variable(&program.ast, "revolve001"),
7443            Some("region001".to_owned())
7444        );
7445        assert_eq!(
7446            region_name_from_sweep_variable(&program.ast, "sweep001"),
7447            Some("region001".to_owned())
7448        );
7449        assert_eq!(
7450            region_name_from_sweep_variable(&program.ast, "loft001"),
7451            Some("region001".to_owned())
7452        );
7453        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7454    }
7455
7456    #[track_caller]
7457    fn expect_sketch(object: &Object) -> &Sketch {
7458        if let ObjectKind::Sketch(sketch) = &object.kind {
7459            sketch
7460        } else {
7461            panic!("Object is not a sketch: {:?}", object);
7462        }
7463    }
7464
7465    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7466        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7467        let ObjectKind::Segment {
7468            segment: Segment::Point(point),
7469        } = &point_object.kind
7470        else {
7471            panic!("Object is not a point segment: {point_object:?}");
7472        };
7473        point.position.clone()
7474    }
7475
7476    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7477        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7478        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7479    }
7480
7481    /// Build a millimeter-valued point expression for concise sketch edit test
7482    /// setup.
7483    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7484        Point2d {
7485            x: Expr::Var(Number {
7486                value: x,
7487                units: NumericSuffix::Mm,
7488            }),
7489            y: Expr::Var(Number {
7490                value: y,
7491                units: NumericSuffix::Mm,
7492            }),
7493        }
7494    }
7495
7496    /// Build a millimeter-valued numeric point for comparing solved scene graph
7497    /// positions.
7498    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7499        Point2d {
7500            x: Number {
7501                value: x,
7502                units: NumericSuffix::Mm,
7503            },
7504            y: Number {
7505                value: y,
7506                units: NumericSuffix::Mm,
7507            },
7508        }
7509    }
7510
7511    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7512        LineCtor {
7513            start: Point2d {
7514                x: Expr::Number(Number { value: start_x, units }),
7515                y: Expr::Number(Number { value: start_y, units }),
7516            },
7517            end: Point2d {
7518                x: Expr::Number(Number { value: end_x, units }),
7519                y: Expr::Number(Number { value: end_y, units }),
7520            },
7521            construction: None,
7522        }
7523    }
7524
7525    async fn create_sketch_with_single_line(
7526        frontend: &mut FrontendState,
7527        ctx: &ExecutorContext,
7528        mock_ctx: &ExecutorContext,
7529        version: Version,
7530    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7531        frontend.program = Program::empty();
7532
7533        let sketch_args = SketchCtor {
7534            on: Plane::Default(PlaneName::Xy),
7535        };
7536        let (_src_delta, _scene_delta, sketch_id) = frontend
7537            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7538            .await
7539            .unwrap();
7540
7541        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7542        let (source_delta, scene_graph_delta) = frontend
7543            .add_segment(mock_ctx, version, sketch_id, segment, None)
7544            .await
7545            .unwrap();
7546        let line_id = *scene_graph_delta
7547            .new_objects
7548            .last()
7549            .expect("Expected line object id to be created");
7550
7551        (sketch_id, line_id, source_delta, scene_graph_delta)
7552    }
7553
7554    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7555        frontend.program = program.clone();
7556        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7557        frontend.update_state_after_exec(outcome, true);
7558    }
7559
7560    #[test]
7561    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7562        for (source, expected_message) in [
7563            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7564            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7565        ] {
7566            let err = parse_frontend_mutation_source(
7567                source,
7568                "Error parsing KCL source after editing",
7569                "No AST produced after editing",
7570            )
7571            .expect_err("expected invalid KCL source to fail");
7572            let message = err.error.message();
7573
7574            assert_eq!(message, expected_message);
7575            assert!(!message.contains("CompilationIssue"));
7576            assert!(!message.contains("KclErrorDetails"));
7577            assert!(!message.contains("source_range"));
7578        }
7579    }
7580
7581    #[tokio::test(flavor = "multi_thread")]
7582    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7583        let initial_source = "\
7584sketch(on = XY) {
7585  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7586  distance([line1.start, line1.end]) == 10
7587}
7588";
7589        let program = Program::parse(initial_source).unwrap().0.unwrap();
7590
7591        let mut frontend = FrontendState::new();
7592        let mock_ctx = ExecutorContext::new_mock(None).await;
7593        let version = Version(0);
7594
7595        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7597        let sketch_id = sketch_object.id;
7598        let sketch = expect_sketch(sketch_object);
7599        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7600
7601        for (value, expected_message) in [
7602            ("**", "Invalid constraint value: Unexpected token: *"),
7603            ("3'", "Invalid constraint value: found unknown token '''"),
7604        ] {
7605            let err = frontend
7606                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7607                .await
7608                .expect_err("expected invalid constraint expression to fail");
7609            let message = err.error.message();
7610
7611            assert_eq!(message, expected_message);
7612            assert!(!message.contains("CompilationIssue"));
7613            assert!(!message.contains("KclErrorDetails"));
7614            assert!(!message.contains("source_range"));
7615        }
7616
7617        mock_ctx.close().await;
7618    }
7619
7620    #[tokio::test(flavor = "multi_thread")]
7621    async fn test_failed_edit_constraint_value_does_not_update_program() {
7622        let initial_source = "\
7623sketch(on = XY) {
7624  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7625  distance([line1.start, line1.end]) == 10
7626}
7627";
7628        let program = Program::parse(initial_source).unwrap().0.unwrap();
7629        let original_source = program.original_file_contents.clone();
7630
7631        let mut frontend = FrontendState::new();
7632        let mock_ctx = ExecutorContext::new_mock(None).await;
7633        let version = Version(0);
7634
7635        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7636        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7637        let sketch_id = sketch_object.id;
7638        let sketch = expect_sketch(sketch_object);
7639        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7640
7641        frontend
7642            .edit_constraint_value(
7643                &mock_ctx,
7644                version,
7645                sketch_id,
7646                constraint_id,
7647                "unknownDistance".to_owned(),
7648            )
7649            .await
7650            .expect_err("expected invalid constraint value to fail execution");
7651
7652        assert_eq!(frontend.program.original_file_contents, original_source);
7653        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7654
7655        mock_ctx.close().await;
7656    }
7657
7658    #[tokio::test(flavor = "multi_thread")]
7659    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7660        let initial_source = "\
7661arr = [0]
7662sketch(on = XY) {
7663  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7664  distance([line1.start, line1.end]) == 10
7665}
7666";
7667        let program = Program::parse(initial_source).unwrap().0.unwrap();
7668
7669        let mut frontend = FrontendState::new();
7670        let mock_ctx = ExecutorContext::new_mock(None).await;
7671        let version = Version(0);
7672
7673        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7674        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7675        let sketch_id = sketch_object.id;
7676        let sketch = expect_sketch(sketch_object);
7677        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7678
7679        // In sketch mode execution, an out-of-bounds array index is an error.
7680        // It should not fall back to the first element of the array the way
7681        // plain mock execution does.
7682        let err = frontend
7683            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7684            .await
7685            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7686        let message = err.error.message();
7687        assert!(
7688            message.contains("The array doesn't have any item at index 5"),
7689            "unexpected error message: {message}"
7690        );
7691
7692        mock_ctx.close().await;
7693    }
7694
7695    #[tokio::test(flavor = "multi_thread")]
7696    async fn test_sketch_checkpoint_round_trip_restores_state() {
7697        let mut frontend = FrontendState::new();
7698        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7699        let mock_ctx = ExecutorContext::new_mock(None).await;
7700        let version = Version(0);
7701
7702        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7703            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7704
7705        let expected_source = source_delta.text.clone();
7706        let expected_scene_graph = frontend.scene_graph.clone();
7707        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7708        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7709
7710        let checkpoint_id = frontend
7711            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7712            .await
7713            .unwrap();
7714
7715        let edited_segments = vec![ExistingSegmentCtor {
7716            id: line_id,
7717            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7718        }];
7719        let (edited_source, _edited_scene) = frontend
7720            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7721            .await
7722            .unwrap();
7723        assert_ne!(edited_source.text, expected_source);
7724
7725        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7726
7727        assert_eq!(restored.source_delta.text, expected_source);
7728        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7729        assert!(restored.scene_graph_delta.invalidates_ids);
7730        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7731        assert_eq!(frontend.scene_graph, expected_scene_graph);
7732        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7733
7734        ctx.close().await;
7735    }
7736
7737    #[tokio::test(flavor = "multi_thread")]
7738    async fn test_sketch_checkpoints_prune_oldest_entries() {
7739        let mut frontend = FrontendState::new();
7740        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7741        let mock_ctx = ExecutorContext::new_mock(None).await;
7742        let version = Version(0);
7743
7744        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7745            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7746
7747        let mut checkpoint_ids = Vec::new();
7748        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7749            checkpoint_ids.push(
7750                frontend
7751                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7752                    .await
7753                    .unwrap(),
7754            );
7755        }
7756
7757        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7758        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7759
7760        let oldest_retained = checkpoint_ids[3];
7761        assert_eq!(
7762            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7763            Some(oldest_retained)
7764        );
7765
7766        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7767        assert!(evicted_restore.is_err());
7768        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7769
7770        frontend
7771            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7772            .await
7773            .unwrap();
7774
7775        ctx.close().await;
7776    }
7777
7778    #[tokio::test(flavor = "multi_thread")]
7779    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7780        let mut frontend = FrontendState::new();
7781        let missing_checkpoint = SketchCheckpointId::new(999);
7782
7783        let err = frontend
7784            .restore_sketch_checkpoint(missing_checkpoint)
7785            .await
7786            .expect_err("Expected restore to fail for missing checkpoint");
7787
7788        assert!(err.msg.contains("Sketch checkpoint not found"));
7789    }
7790
7791    #[tokio::test(flavor = "multi_thread")]
7792    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7793        let mut frontend = FrontendState::new();
7794        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7795        let mock_ctx = ExecutorContext::new_mock(None).await;
7796        let version = Version(0);
7797
7798        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7799            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7800
7801        let checkpoint_a = frontend
7802            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7803            .await
7804            .unwrap();
7805        let checkpoint_b = frontend
7806            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7807            .await
7808            .unwrap();
7809        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7810
7811        frontend.clear_sketch_checkpoints();
7812        assert!(frontend.sketch_checkpoints.is_empty());
7813        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7814        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7815
7816        ctx.close().await;
7817    }
7818
7819    #[tokio::test(flavor = "multi_thread")]
7820    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7821        let mut frontend = FrontendState::new();
7822        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7823        let mock_ctx = ExecutorContext::new_mock(None).await;
7824        let version = Version(0);
7825
7826        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7827            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7828        let old_source = source_delta.text.clone();
7829        let old_checkpoint = frontend
7830            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7831            .await
7832            .unwrap();
7833        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7834
7835        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7836            .unwrap()
7837            .0
7838            .unwrap();
7839
7840        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7841        let SetProgramOutcome::Success {
7842            checkpoint_id: Some(new_checkpoint),
7843            ..
7844        } = result
7845        else {
7846            panic!("Expected Success with a fresh checkpoint baseline");
7847        };
7848
7849        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7850
7851        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7852        assert_eq!(old_restore.source_delta.text, old_source);
7853
7854        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7855        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7856
7857        ctx.close().await;
7858    }
7859
7860    #[tokio::test(flavor = "multi_thread")]
7861    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7862        let mut frontend = FrontendState::new();
7863        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7864        let mock_ctx = ExecutorContext::new_mock(None).await;
7865        let version = Version(0);
7866
7867        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7868            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7869        let old_checkpoint = frontend
7870            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7871            .await
7872            .unwrap();
7873        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7874
7875        let failing_program = Program::parse(
7876            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7877        )
7878        .unwrap()
7879        .0
7880        .unwrap();
7881
7882        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7883        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7884        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7885        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7886
7887        ctx.close().await;
7888    }
7889
7890    #[tokio::test(flavor = "multi_thread")]
7891    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7892        let mut frontend = FrontendState::new();
7893        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7894
7895        let program = Program::parse(
7896            "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",
7897        )
7898        .unwrap()
7899        .0
7900        .unwrap();
7901        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7902        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7903            panic!("Expected successful baseline program execution");
7904        };
7905
7906        clear_mem_cache().await;
7907        assert!(read_old_memory().await.is_none());
7908
7909        let checkpoint_without_mock_memory = frontend
7910            .create_sketch_checkpoint((*exec_outcome).clone())
7911            .await
7912            .unwrap();
7913
7914        write_old_memory(SketchModeState::new_for_tests()).await;
7915        assert!(read_old_memory().await.is_some());
7916
7917        let checkpoint_with_mock_memory = frontend
7918            .create_sketch_checkpoint((*exec_outcome).clone())
7919            .await
7920            .unwrap();
7921
7922        clear_mem_cache().await;
7923        assert!(read_old_memory().await.is_none());
7924
7925        frontend
7926            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7927            .await
7928            .unwrap();
7929        assert!(read_old_memory().await.is_some());
7930
7931        frontend
7932            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7933            .await
7934            .unwrap();
7935        assert!(read_old_memory().await.is_none());
7936
7937        ctx.close().await;
7938    }
7939
7940    #[tokio::test(flavor = "multi_thread")]
7941    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7942        let source = "\
7943sketch(on = XY) {
7944  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7945}
7946
7947bad = missing_name
7948";
7949        let program = Program::parse(source).unwrap().0.unwrap();
7950
7951        let mut frontend = FrontendState::new();
7952
7953        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7954        let mock_ctx = ExecutorContext::new_mock(None).await;
7955        let version = Version(0);
7956        let project_id = ProjectId(0);
7957        let file_id = FileId(0);
7958
7959        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7960            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7961        };
7962
7963        let sketch_id = frontend
7964            .scene_graph
7965            .objects
7966            .iter()
7967            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7968            .expect("Expected sketch object from errored hack_set_program");
7969
7970        frontend
7971            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7972            .await
7973            .unwrap();
7974
7975        ctx.close().await;
7976        mock_ctx.close().await;
7977    }
7978
7979    #[tokio::test(flavor = "multi_thread")]
7980    async fn test_new_sketch_add_point_edit_point() {
7981        let program = Program::empty();
7982
7983        let mut frontend = FrontendState::new();
7984        frontend.program = program;
7985
7986        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7987        let mock_ctx = ExecutorContext::new_mock(None).await;
7988        let version = Version(0);
7989
7990        let sketch_args = SketchCtor {
7991            on: Plane::Default(PlaneName::Xy),
7992        };
7993        let (_src_delta, scene_delta, sketch_id) = frontend
7994            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7995            .await
7996            .unwrap();
7997        assert_eq!(sketch_id, ObjectId(1));
7998        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7999        let sketch_object = &scene_delta.new_graph.objects[1];
8000        assert_eq!(sketch_object.id, ObjectId(1));
8001        assert_eq!(
8002            sketch_object.kind,
8003            ObjectKind::Sketch(Sketch {
8004                args: SketchCtor {
8005                    on: Plane::Default(PlaneName::Xy)
8006                },
8007                plane: ObjectId(0),
8008                segments: vec![],
8009                constraints: vec![],
8010            })
8011        );
8012        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8013
8014        let point_ctor = PointCtor {
8015            position: Point2d {
8016                x: Expr::Number(Number {
8017                    value: 1.0,
8018                    units: NumericSuffix::Inch,
8019                }),
8020                y: Expr::Number(Number {
8021                    value: 2.0,
8022                    units: NumericSuffix::Inch,
8023                }),
8024            },
8025        };
8026        let segment = SegmentCtor::Point(point_ctor);
8027        let (src_delta, scene_delta) = frontend
8028            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8029            .await
8030            .unwrap();
8031        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8032        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8033        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8034        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8035            assert_eq!(scene_object.id.0, i);
8036        }
8037
8038        let point_id = *scene_delta.new_objects.last().unwrap();
8039
8040        let point_ctor = PointCtor {
8041            position: Point2d {
8042                x: Expr::Number(Number {
8043                    value: 3.0,
8044                    units: NumericSuffix::Inch,
8045                }),
8046                y: Expr::Number(Number {
8047                    value: 4.0,
8048                    units: NumericSuffix::Inch,
8049                }),
8050            },
8051        };
8052        let segments = vec![ExistingSegmentCtor {
8053            id: point_id,
8054            ctor: SegmentCtor::Point(point_ctor),
8055        }];
8056        let (src_delta, scene_delta) = frontend
8057            .edit_segments(&mock_ctx, version, sketch_id, segments)
8058            .await
8059            .unwrap();
8060        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8061        assert_eq!(scene_delta.new_objects, vec![]);
8062        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8063
8064        ctx.close().await;
8065        mock_ctx.close().await;
8066    }
8067
8068    #[tokio::test(flavor = "multi_thread")]
8069    async fn test_new_sketch_add_line_edit_line() {
8070        let program = Program::empty();
8071
8072        let mut frontend = FrontendState::new();
8073        frontend.program = program;
8074
8075        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8076        let mock_ctx = ExecutorContext::new_mock(None).await;
8077        let version = Version(0);
8078
8079        let sketch_args = SketchCtor {
8080            on: Plane::Default(PlaneName::Xy),
8081        };
8082        let (_src_delta, scene_delta, sketch_id) = frontend
8083            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8084            .await
8085            .unwrap();
8086        assert_eq!(sketch_id, ObjectId(1));
8087        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8088        let sketch_object = &scene_delta.new_graph.objects[1];
8089        assert_eq!(sketch_object.id, ObjectId(1));
8090        assert_eq!(
8091            sketch_object.kind,
8092            ObjectKind::Sketch(Sketch {
8093                args: SketchCtor {
8094                    on: Plane::Default(PlaneName::Xy)
8095                },
8096                plane: ObjectId(0),
8097                segments: vec![],
8098                constraints: vec![],
8099            })
8100        );
8101        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8102
8103        let line_ctor = LineCtor {
8104            start: Point2d {
8105                x: Expr::Number(Number {
8106                    value: 0.0,
8107                    units: NumericSuffix::Mm,
8108                }),
8109                y: Expr::Number(Number {
8110                    value: 0.0,
8111                    units: NumericSuffix::Mm,
8112                }),
8113            },
8114            end: Point2d {
8115                x: Expr::Number(Number {
8116                    value: 10.0,
8117                    units: NumericSuffix::Mm,
8118                }),
8119                y: Expr::Number(Number {
8120                    value: 10.0,
8121                    units: NumericSuffix::Mm,
8122                }),
8123            },
8124            construction: None,
8125        };
8126        let segment = SegmentCtor::Line(line_ctor);
8127        let (src_delta, scene_delta) = frontend
8128            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8129            .await
8130            .unwrap();
8131        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8132        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8133        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8134        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8135            assert_eq!(scene_object.id.0, i);
8136        }
8137
8138        // The new objects are the end points and then the line.
8139        let line = *scene_delta.new_objects.last().unwrap();
8140
8141        let line_ctor = LineCtor {
8142            start: Point2d {
8143                x: Expr::Number(Number {
8144                    value: 1.0,
8145                    units: NumericSuffix::Mm,
8146                }),
8147                y: Expr::Number(Number {
8148                    value: 2.0,
8149                    units: NumericSuffix::Mm,
8150                }),
8151            },
8152            end: Point2d {
8153                x: Expr::Number(Number {
8154                    value: 13.0,
8155                    units: NumericSuffix::Mm,
8156                }),
8157                y: Expr::Number(Number {
8158                    value: 14.0,
8159                    units: NumericSuffix::Mm,
8160                }),
8161            },
8162            construction: None,
8163        };
8164        let segments = vec![ExistingSegmentCtor {
8165            id: line,
8166            ctor: SegmentCtor::Line(line_ctor),
8167        }];
8168        let (src_delta, scene_delta) = frontend
8169            .edit_segments(&mock_ctx, version, sketch_id, segments)
8170            .await
8171            .unwrap();
8172        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8173        assert_eq!(scene_delta.new_objects, vec![]);
8174        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8175
8176        ctx.close().await;
8177        mock_ctx.close().await;
8178    }
8179
8180    #[tokio::test(flavor = "multi_thread")]
8181    async fn test_new_sketch_add_arc_edit_arc() {
8182        let program = Program::empty();
8183
8184        let mut frontend = FrontendState::new();
8185        frontend.program = program;
8186
8187        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8188        let mock_ctx = ExecutorContext::new_mock(None).await;
8189        let version = Version(0);
8190
8191        let sketch_args = SketchCtor {
8192            on: Plane::Default(PlaneName::Xy),
8193        };
8194        let (_src_delta, scene_delta, sketch_id) = frontend
8195            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8196            .await
8197            .unwrap();
8198        assert_eq!(sketch_id, ObjectId(1));
8199        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8200        let sketch_object = &scene_delta.new_graph.objects[1];
8201        assert_eq!(sketch_object.id, ObjectId(1));
8202        assert_eq!(
8203            sketch_object.kind,
8204            ObjectKind::Sketch(Sketch {
8205                args: SketchCtor {
8206                    on: Plane::Default(PlaneName::Xy),
8207                },
8208                plane: ObjectId(0),
8209                segments: vec![],
8210                constraints: vec![],
8211            })
8212        );
8213        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8214
8215        let arc_ctor = ArcCtor {
8216            start: Point2d {
8217                x: Expr::Var(Number {
8218                    value: 0.0,
8219                    units: NumericSuffix::Mm,
8220                }),
8221                y: Expr::Var(Number {
8222                    value: 0.0,
8223                    units: NumericSuffix::Mm,
8224                }),
8225            },
8226            end: Point2d {
8227                x: Expr::Var(Number {
8228                    value: 10.0,
8229                    units: NumericSuffix::Mm,
8230                }),
8231                y: Expr::Var(Number {
8232                    value: 10.0,
8233                    units: NumericSuffix::Mm,
8234                }),
8235            },
8236            center: Point2d {
8237                x: Expr::Var(Number {
8238                    value: 10.0,
8239                    units: NumericSuffix::Mm,
8240                }),
8241                y: Expr::Var(Number {
8242                    value: 0.0,
8243                    units: NumericSuffix::Mm,
8244                }),
8245            },
8246            direction: None,
8247            construction: None,
8248        };
8249        let segment = SegmentCtor::Arc(arc_ctor);
8250        let (src_delta, scene_delta) = frontend
8251            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8252            .await
8253            .unwrap();
8254        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8255        assert_eq!(
8256            scene_delta.new_objects,
8257            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8258        );
8259        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8260            assert_eq!(scene_object.id.0, i);
8261        }
8262        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8263
8264        // The new objects are the end points, the center, and then the arc.
8265        let arc = *scene_delta.new_objects.last().unwrap();
8266
8267        let arc_ctor = ArcCtor {
8268            start: Point2d {
8269                x: Expr::Var(Number {
8270                    value: 1.0,
8271                    units: NumericSuffix::Mm,
8272                }),
8273                y: Expr::Var(Number {
8274                    value: 2.0,
8275                    units: NumericSuffix::Mm,
8276                }),
8277            },
8278            end: Point2d {
8279                x: Expr::Var(Number {
8280                    value: 13.0,
8281                    units: NumericSuffix::Mm,
8282                }),
8283                y: Expr::Var(Number {
8284                    value: 14.0,
8285                    units: NumericSuffix::Mm,
8286                }),
8287            },
8288            center: Point2d {
8289                x: Expr::Var(Number {
8290                    value: 13.0,
8291                    units: NumericSuffix::Mm,
8292                }),
8293                y: Expr::Var(Number {
8294                    value: 2.0,
8295                    units: NumericSuffix::Mm,
8296                }),
8297            },
8298            direction: None,
8299            construction: None,
8300        };
8301        let segments = vec![ExistingSegmentCtor {
8302            id: arc,
8303            ctor: SegmentCtor::Arc(arc_ctor),
8304        }];
8305        let (src_delta, scene_delta) = frontend
8306            .edit_segments(&mock_ctx, version, sketch_id, segments)
8307            .await
8308            .unwrap();
8309        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8310        assert_eq!(scene_delta.new_objects, vec![]);
8311        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8312
8313        ctx.close().await;
8314        mock_ctx.close().await;
8315    }
8316
8317    #[tokio::test(flavor = "multi_thread")]
8318    async fn test_new_sketch_add_circle_edit_circle() {
8319        let program = Program::empty();
8320
8321        let mut frontend = FrontendState::new();
8322        frontend.program = program;
8323
8324        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8325        let mock_ctx = ExecutorContext::new_mock(None).await;
8326        let version = Version(0);
8327
8328        let sketch_args = SketchCtor {
8329            on: Plane::Default(PlaneName::Xy),
8330        };
8331        let (_src_delta, _scene_delta, sketch_id) = frontend
8332            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8333            .await
8334            .unwrap();
8335
8336        // Add a circle segment.
8337        let circle_ctor = CircleCtor {
8338            start: Point2d {
8339                x: Expr::Var(Number {
8340                    value: 5.0,
8341                    units: NumericSuffix::Mm,
8342                }),
8343                y: Expr::Var(Number {
8344                    value: 0.0,
8345                    units: NumericSuffix::Mm,
8346                }),
8347            },
8348            center: Point2d {
8349                x: Expr::Var(Number {
8350                    value: 0.0,
8351                    units: NumericSuffix::Mm,
8352                }),
8353                y: Expr::Var(Number {
8354                    value: 0.0,
8355                    units: NumericSuffix::Mm,
8356                }),
8357            },
8358            construction: None,
8359        };
8360        let segment = SegmentCtor::Circle(circle_ctor);
8361        let (src_delta, scene_delta) = frontend
8362            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8363            .await
8364            .unwrap();
8365        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8366        // The new objects are start, center, and then the circle segment.
8367        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8368        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8369
8370        let circle = *scene_delta.new_objects.last().unwrap();
8371
8372        // Edit the circle segment.
8373        let circle_ctor = CircleCtor {
8374            start: Point2d {
8375                x: Expr::Var(Number {
8376                    value: 10.0,
8377                    units: NumericSuffix::Mm,
8378                }),
8379                y: Expr::Var(Number {
8380                    value: 0.0,
8381                    units: NumericSuffix::Mm,
8382                }),
8383            },
8384            center: Point2d {
8385                x: Expr::Var(Number {
8386                    value: 3.0,
8387                    units: NumericSuffix::Mm,
8388                }),
8389                y: Expr::Var(Number {
8390                    value: 4.0,
8391                    units: NumericSuffix::Mm,
8392                }),
8393            },
8394            construction: None,
8395        };
8396        let segments = vec![ExistingSegmentCtor {
8397            id: circle,
8398            ctor: SegmentCtor::Circle(circle_ctor),
8399        }];
8400        let (src_delta, scene_delta) = frontend
8401            .edit_segments(&mock_ctx, version, sketch_id, segments)
8402            .await
8403            .unwrap();
8404        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8405        assert_eq!(scene_delta.new_objects, vec![]);
8406        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8407
8408        ctx.close().await;
8409        mock_ctx.close().await;
8410    }
8411
8412    #[tokio::test(flavor = "multi_thread")]
8413    async fn test_delete_circle() {
8414        let initial_source = "sketch001 = sketch(on = XY) {
8415  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8416}
8417";
8418
8419        let program = Program::parse(initial_source).unwrap().0.unwrap();
8420        let mut frontend = FrontendState::new();
8421
8422        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8423        let mock_ctx = ExecutorContext::new_mock(None).await;
8424        let version = Version(0);
8425
8426        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8427        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8428        let sketch_id = sketch_object.id;
8429        let sketch = expect_sketch(sketch_object);
8430
8431        // The sketch should have 3 segments: start point, center point, and the circle.
8432        assert_eq!(sketch.segments.len(), 3);
8433        let circle_id = sketch.segments[2];
8434
8435        // Delete the circle.
8436        let (src_delta, scene_delta) = frontend
8437            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8438            .await
8439            .unwrap();
8440        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8441        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8442        let new_sketch = expect_sketch(new_sketch_object);
8443        assert_eq!(new_sketch.segments.len(), 0);
8444
8445        ctx.close().await;
8446        mock_ctx.close().await;
8447    }
8448
8449    #[tokio::test(flavor = "multi_thread")]
8450    async fn test_edit_circle_via_point() {
8451        let initial_source = "sketch001 = sketch(on = XY) {
8452  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8453}
8454";
8455
8456        let program = Program::parse(initial_source).unwrap().0.unwrap();
8457        let mut frontend = FrontendState::new();
8458
8459        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8460        let mock_ctx = ExecutorContext::new_mock(None).await;
8461        let version = Version(0);
8462
8463        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8464        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8465        let sketch_id = sketch_object.id;
8466        let sketch = expect_sketch(sketch_object);
8467
8468        // Find the circle segment and its start point.
8469        let circle_id = sketch
8470            .segments
8471            .iter()
8472            .copied()
8473            .find(|seg_id| {
8474                matches!(
8475                    &frontend.scene_graph.objects[seg_id.0].kind,
8476                    ObjectKind::Segment {
8477                        segment: Segment::Circle(_)
8478                    }
8479                )
8480            })
8481            .expect("Expected a circle segment in sketch");
8482        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8483        let ObjectKind::Segment {
8484            segment: Segment::Circle(circle),
8485        } = &circle_object.kind
8486        else {
8487            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8488        };
8489        let start_point_id = circle.start;
8490
8491        // Edit the start point via SegmentCtor::Point.
8492        let segments = vec![ExistingSegmentCtor {
8493            id: start_point_id,
8494            ctor: SegmentCtor::Point(PointCtor {
8495                position: Point2d {
8496                    x: Expr::Var(Number {
8497                        value: 7.0,
8498                        units: NumericSuffix::Mm,
8499                    }),
8500                    y: Expr::Var(Number {
8501                        value: 1.0,
8502                        units: NumericSuffix::Mm,
8503                    }),
8504                },
8505            }),
8506        }];
8507        let (src_delta, _scene_delta) = frontend
8508            .edit_segments(&mock_ctx, version, sketch_id, segments)
8509            .await
8510            .unwrap();
8511        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8512
8513        ctx.close().await;
8514        mock_ctx.close().await;
8515    }
8516
8517    #[tokio::test(flavor = "multi_thread")]
8518    async fn test_add_line_when_sketch_block_uses_variable() {
8519        let initial_source = "s = sketch(on = XY) {}
8520";
8521
8522        let program = Program::parse(initial_source).unwrap().0.unwrap();
8523
8524        let mut frontend = FrontendState::new();
8525
8526        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8527        let mock_ctx = ExecutorContext::new_mock(None).await;
8528        let version = Version(0);
8529
8530        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8531        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8532        let sketch_id = sketch_object.id;
8533
8534        let line_ctor = LineCtor {
8535            start: Point2d {
8536                x: Expr::Number(Number {
8537                    value: 0.0,
8538                    units: NumericSuffix::Mm,
8539                }),
8540                y: Expr::Number(Number {
8541                    value: 0.0,
8542                    units: NumericSuffix::Mm,
8543                }),
8544            },
8545            end: Point2d {
8546                x: Expr::Number(Number {
8547                    value: 10.0,
8548                    units: NumericSuffix::Mm,
8549                }),
8550                y: Expr::Number(Number {
8551                    value: 10.0,
8552                    units: NumericSuffix::Mm,
8553                }),
8554            },
8555            construction: None,
8556        };
8557        let segment = SegmentCtor::Line(line_ctor);
8558        let (src_delta, scene_delta) = frontend
8559            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8560            .await
8561            .unwrap();
8562        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8563        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8564        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8565
8566        ctx.close().await;
8567        mock_ctx.close().await;
8568    }
8569
8570    #[tokio::test(flavor = "multi_thread")]
8571    async fn test_new_sketch_add_line_delete_sketch() {
8572        let program = Program::empty();
8573
8574        let mut frontend = FrontendState::new();
8575        frontend.program = program;
8576
8577        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8578        let mock_ctx = ExecutorContext::new_mock(None).await;
8579        let version = Version(0);
8580
8581        let sketch_args = SketchCtor {
8582            on: Plane::Default(PlaneName::Xy),
8583        };
8584        let (_src_delta, scene_delta, sketch_id) = frontend
8585            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8586            .await
8587            .unwrap();
8588        assert_eq!(sketch_id, ObjectId(1));
8589        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8590        let sketch_object = &scene_delta.new_graph.objects[1];
8591        assert_eq!(sketch_object.id, ObjectId(1));
8592        assert_eq!(
8593            sketch_object.kind,
8594            ObjectKind::Sketch(Sketch {
8595                args: SketchCtor {
8596                    on: Plane::Default(PlaneName::Xy)
8597                },
8598                plane: ObjectId(0),
8599                segments: vec![],
8600                constraints: vec![],
8601            })
8602        );
8603        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8604
8605        let line_ctor = LineCtor {
8606            start: Point2d {
8607                x: Expr::Number(Number {
8608                    value: 0.0,
8609                    units: NumericSuffix::Mm,
8610                }),
8611                y: Expr::Number(Number {
8612                    value: 0.0,
8613                    units: NumericSuffix::Mm,
8614                }),
8615            },
8616            end: Point2d {
8617                x: Expr::Number(Number {
8618                    value: 10.0,
8619                    units: NumericSuffix::Mm,
8620                }),
8621                y: Expr::Number(Number {
8622                    value: 10.0,
8623                    units: NumericSuffix::Mm,
8624                }),
8625            },
8626            construction: None,
8627        };
8628        let segment = SegmentCtor::Line(line_ctor);
8629        let (src_delta, scene_delta) = frontend
8630            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8631            .await
8632            .unwrap();
8633        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8634        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8635
8636        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8637        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8638        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8639
8640        ctx.close().await;
8641        mock_ctx.close().await;
8642    }
8643
8644    #[tokio::test(flavor = "multi_thread")]
8645    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8646        let initial_source = "s = sketch(on = XY) {}
8647";
8648
8649        let program = Program::parse(initial_source).unwrap().0.unwrap();
8650
8651        let mut frontend = FrontendState::new();
8652
8653        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8654        let version = Version(0);
8655
8656        frontend.hack_set_program(&ctx, program).await.unwrap();
8657        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8658        let sketch_id = sketch_object.id;
8659
8660        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8661        insta::assert_snapshot!(
8662            "test_delete_sketch_when_sketch_block_uses_variable",
8663            src_delta.text.as_str()
8664        );
8665        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8666
8667        ctx.close().await;
8668    }
8669
8670    #[tokio::test(flavor = "multi_thread")]
8671    async fn test_delete_sketch_after_comment() {
8672        let initial_source = "sketch001 = sketch(on = XZ) {
8673}
8674";
8675
8676        let program = Program::parse(initial_source).unwrap().0.unwrap();
8677        let mut frontend = FrontendState::new();
8678
8679        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8680        let version = Version(0);
8681
8682        frontend.hack_set_program(&ctx, program).await.unwrap();
8683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8684        let sketch_id = sketch_object.id;
8685        let original_source = sketch_object.source.clone();
8686
8687        let commented_source = "// test 1
8688sketch001 = sketch(on = XZ) {
8689}
8690";
8691        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8692        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8693
8694        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8695        assert_eq!(cached_sketch_object.source, original_source);
8696
8697        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8698        assert!(
8699            !src_delta.text.contains("sketch001"),
8700            "sketch was not deleted: {}",
8701            src_delta.text
8702        );
8703        // The leading line comment must survive deletion.
8704        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8705        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8706
8707        ctx.close().await;
8708    }
8709
8710    #[tokio::test(flavor = "multi_thread")]
8711    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8712        let initial_source = "sketch001 = sketch(on = XZ) {
8713}
8714foo = 1
8715";
8716
8717        let program = Program::parse(initial_source).unwrap().0.unwrap();
8718        let mut frontend = FrontendState::new();
8719
8720        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8721        let version = Version(0);
8722
8723        frontend.hack_set_program(&ctx, program).await.unwrap();
8724        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8725        let sketch_id = sketch_object.id;
8726
8727        let commented_source = "// keep me
8728sketch001 = sketch(on = XZ) {
8729}
8730foo = 1
8731";
8732        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8733        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8734
8735        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8736        // The leading comment should remain, now attached to the following body item.
8737        insta::assert_snapshot!(
8738            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8739            src_delta.text.as_str()
8740        );
8741
8742        ctx.close().await;
8743    }
8744
8745    #[tokio::test(flavor = "multi_thread")]
8746    async fn test_delete_segment_preserves_pre_comment() {
8747        let initial_source = "\
8748sketch(on = XY) {
8749  point(at = [var 1, var 2])
8750  // describe the middle point
8751  point(at = [var 3, var 4])
8752  point(at = [var 5, var 6])
8753}
8754";
8755
8756        let program = Program::parse(initial_source).unwrap().0.unwrap();
8757        let mut frontend = FrontendState::new();
8758
8759        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8760        let mock_ctx = ExecutorContext::new_mock(None).await;
8761        let version = Version(0);
8762
8763        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8764        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8765        let sketch_id = sketch_object.id;
8766        let sketch = expect_sketch(sketch_object);
8767
8768        let middle_point_id = *sketch.segments.get(1).unwrap();
8769
8770        let (src_delta, _scene_delta) = frontend
8771            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8772            .await
8773            .unwrap();
8774        // The line comment on the line above the deleted point must be preserved.
8775        // It is reattached to the next surviving body item.
8776        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8777
8778        ctx.close().await;
8779        mock_ctx.close().await;
8780    }
8781
8782    #[tokio::test(flavor = "multi_thread")]
8783    async fn test_delete_last_segment_preserves_pre_comment() {
8784        let initial_source = "\
8785sketch(on = XY) {
8786  point(at = [var 1, var 2])
8787  // describe the trailing point
8788  point(at = [var 3, var 4])
8789}
8790";
8791
8792        let program = Program::parse(initial_source).unwrap().0.unwrap();
8793        let mut frontend = FrontendState::new();
8794
8795        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8796        let mock_ctx = ExecutorContext::new_mock(None).await;
8797        let version = Version(0);
8798
8799        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8801        let sketch_id = sketch_object.id;
8802        let sketch = expect_sketch(sketch_object);
8803
8804        let last_point_id = *sketch.segments.last().unwrap();
8805
8806        let (src_delta, _scene_delta) = frontend
8807            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8808            .await
8809            .unwrap();
8810        // No following item to attach to; the comment is kept inside the sketch
8811        // block as trailing non-code metadata so the user does not lose it.
8812        insta::assert_snapshot!(
8813            "test_delete_last_segment_preserves_pre_comment",
8814            src_delta.text.as_str()
8815        );
8816
8817        ctx.close().await;
8818        mock_ctx.close().await;
8819    }
8820
8821    #[tokio::test(flavor = "multi_thread")]
8822    async fn test_delete_segment_drops_inline_trailing_comment() {
8823        let initial_source = "\
8824sketch(on = XY) {
8825  point(at = [var 1, var 2])
8826  point(at = [var 3, var 4]) // same-line note that gets dropped
8827  point(at = [var 5, var 6])
8828}
8829";
8830
8831        let program = Program::parse(initial_source).unwrap().0.unwrap();
8832        let mut frontend = FrontendState::new();
8833
8834        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8835        let mock_ctx = ExecutorContext::new_mock(None).await;
8836        let version = Version(0);
8837
8838        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8839        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8840        let sketch_id = sketch_object.id;
8841        let sketch = expect_sketch(sketch_object);
8842
8843        let middle_point_id = *sketch.segments.get(1).unwrap();
8844
8845        let (src_delta, _scene_delta) = frontend
8846            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8847            .await
8848            .unwrap();
8849        // The same-line trailing comment is removed along with the deleted code.
8850        assert!(
8851            !src_delta.text.contains("same-line note"),
8852            "inline comment should have been removed: {}",
8853            src_delta.text
8854        );
8855
8856        ctx.close().await;
8857        mock_ctx.close().await;
8858    }
8859
8860    #[tokio::test(flavor = "multi_thread")]
8861    async fn test_delete_segments_preserves_block_comments_across_positions() {
8862        // One test exercising several `delete_body_item_preserving_pre_comments`
8863        // branches at once with `/* ... */` block comments:
8864        //   - first point: leading block comment must migrate to the next item.
8865        //   - first point: same-line trailing block comment must be dropped.
8866        //   - middle point: leading block comment must stay attached after migration.
8867        //   - last point: leading block comment, with no surviving next item,
8868        //     must be converted into a trailing NonCodeNode.
8869        let initial_source = "\
8870sketch(on = XY) {
8871  /* above first - moves to middle */
8872  point(at = [var 1, var 2]) /* same-line on first - dropped */
8873  /* above middle - stays */
8874  point(at = [var 3, var 4])
8875  /* above last - moves to trailing meta */
8876  point(at = [var 5, var 6])
8877}
8878";
8879
8880        let program = Program::parse(initial_source).unwrap().0.unwrap();
8881        let mut frontend = FrontendState::new();
8882
8883        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8884        let mock_ctx = ExecutorContext::new_mock(None).await;
8885        let version = Version(0);
8886
8887        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8888        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8889        let sketch_id = sketch_object.id;
8890        let sketch = expect_sketch(sketch_object);
8891
8892        let first_point_id = *sketch.segments.first().unwrap();
8893        let last_point_id = *sketch.segments.last().unwrap();
8894
8895        let (src_delta, _scene_delta) = frontend
8896            .delete_objects(
8897                &mock_ctx,
8898                version,
8899                sketch_id,
8900                Vec::new(),
8901                vec![first_point_id, last_point_id],
8902            )
8903            .await
8904            .unwrap();
8905        insta::assert_snapshot!(
8906            "test_delete_segments_preserves_block_comments_across_positions",
8907            src_delta.text.as_str()
8908        );
8909
8910        ctx.close().await;
8911        mock_ctx.close().await;
8912    }
8913
8914    #[tokio::test(flavor = "multi_thread")]
8915    async fn test_edit_line_when_editing_its_start_point() {
8916        let initial_source = "\
8917sketch(on = XY) {
8918  line(start = [var 1, var 2], end = [var 3, var 4])
8919}
8920";
8921
8922        let program = Program::parse(initial_source).unwrap().0.unwrap();
8923
8924        let mut frontend = FrontendState::new();
8925
8926        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8927        let mock_ctx = ExecutorContext::new_mock(None).await;
8928        let version = Version(0);
8929
8930        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8931        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8932        let sketch_id = sketch_object.id;
8933        let sketch = expect_sketch(sketch_object);
8934
8935        let point_id = *sketch.segments.first().unwrap();
8936
8937        let point_ctor = PointCtor {
8938            position: Point2d {
8939                x: Expr::Var(Number {
8940                    value: 5.0,
8941                    units: NumericSuffix::Inch,
8942                }),
8943                y: Expr::Var(Number {
8944                    value: 6.0,
8945                    units: NumericSuffix::Inch,
8946                }),
8947            },
8948        };
8949        let segments = vec![ExistingSegmentCtor {
8950            id: point_id,
8951            ctor: SegmentCtor::Point(point_ctor),
8952        }];
8953        let (src_delta, scene_delta) = frontend
8954            .edit_segments(&mock_ctx, version, sketch_id, segments)
8955            .await
8956            .unwrap();
8957        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8958        assert_eq!(scene_delta.new_objects, vec![]);
8959        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8960
8961        ctx.close().await;
8962        mock_ctx.close().await;
8963    }
8964
8965    #[tokio::test(flavor = "multi_thread")]
8966    async fn test_edit_line_when_editing_its_end_point() {
8967        let initial_source = "\
8968sketch(on = XY) {
8969  line(start = [var 1, var 2], end = [var 3, var 4])
8970}
8971";
8972
8973        let program = Program::parse(initial_source).unwrap().0.unwrap();
8974
8975        let mut frontend = FrontendState::new();
8976
8977        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8978        let mock_ctx = ExecutorContext::new_mock(None).await;
8979        let version = Version(0);
8980
8981        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8982        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8983        let sketch_id = sketch_object.id;
8984        let sketch = expect_sketch(sketch_object);
8985        let point_id = *sketch.segments.get(1).unwrap();
8986
8987        let point_ctor = PointCtor {
8988            position: Point2d {
8989                x: Expr::Var(Number {
8990                    value: 5.0,
8991                    units: NumericSuffix::Inch,
8992                }),
8993                y: Expr::Var(Number {
8994                    value: 6.0,
8995                    units: NumericSuffix::Inch,
8996                }),
8997            },
8998        };
8999        let segments = vec![ExistingSegmentCtor {
9000            id: point_id,
9001            ctor: SegmentCtor::Point(point_ctor),
9002        }];
9003        let (src_delta, scene_delta) = frontend
9004            .edit_segments(&mock_ctx, version, sketch_id, segments)
9005            .await
9006            .unwrap();
9007        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9008        assert_eq!(scene_delta.new_objects, vec![]);
9009        assert_eq!(
9010            scene_delta.new_graph.objects.len(),
9011            5,
9012            "{:#?}",
9013            scene_delta.new_graph.objects
9014        );
9015
9016        ctx.close().await;
9017        mock_ctx.close().await;
9018    }
9019
9020    #[tokio::test(flavor = "multi_thread")]
9021    async fn test_edit_line_with_coincident_feedback() {
9022        let initial_source = "\
9023sketch(on = XY) {
9024  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9025  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9026  fixed([line1.start, [0, 0]])
9027  coincident([line1.end, line2.start])
9028  equalLength([line1, line2])
9029}
9030";
9031
9032        let program = Program::parse(initial_source).unwrap().0.unwrap();
9033
9034        let mut frontend = FrontendState::new();
9035
9036        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9037        let mock_ctx = ExecutorContext::new_mock(None).await;
9038        let version = Version(0);
9039
9040        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9041        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9042        let sketch_id = sketch_object.id;
9043        let sketch = expect_sketch(sketch_object);
9044        let line2_end_id = *sketch.segments.get(4).unwrap();
9045
9046        let segments = vec![ExistingSegmentCtor {
9047            id: line2_end_id,
9048            ctor: SegmentCtor::Point(PointCtor {
9049                position: Point2d {
9050                    x: Expr::Var(Number {
9051                        value: 9.0,
9052                        units: NumericSuffix::None,
9053                    }),
9054                    y: Expr::Var(Number {
9055                        value: 10.0,
9056                        units: NumericSuffix::None,
9057                    }),
9058                },
9059            }),
9060        }];
9061        let (src_delta, scene_delta) = frontend
9062            .edit_segments(&mock_ctx, version, sketch_id, segments)
9063            .await
9064            .unwrap();
9065        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9066        assert_eq!(
9067            scene_delta.new_graph.objects.len(),
9068            11,
9069            "{:#?}",
9070            scene_delta.new_graph.objects
9071        );
9072
9073        ctx.close().await;
9074        mock_ctx.close().await;
9075    }
9076
9077    #[tokio::test(flavor = "multi_thread")]
9078    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9079        let initial_source = "\
9080sketch(on = XY) {
9081  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9082  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9083  fixed([line1.start, [0, 0]])
9084  coincident([line1.end, line2.start])
9085  equalLength([line1, line2])
9086}
9087";
9088
9089        let program = Program::parse(initial_source).unwrap().0.unwrap();
9090        let mut frontend = FrontendState::new();
9091        let mock_ctx = ExecutorContext::new_mock(None).await;
9092        let version = Version(0);
9093
9094        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9095        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9096        let sketch_id = sketch_object.id;
9097        let sketch = expect_sketch(sketch_object);
9098        let line2_end_id = *sketch.segments.get(4).unwrap();
9099
9100        let segments = vec![ExistingSegmentCtor {
9101            id: line2_end_id,
9102            ctor: SegmentCtor::Point(PointCtor {
9103                position: Point2d {
9104                    x: Expr::Var(Number {
9105                        value: 9.0,
9106                        units: NumericSuffix::None,
9107                    }),
9108                    y: Expr::Var(Number {
9109                        value: 10.0,
9110                        units: NumericSuffix::None,
9111                    }),
9112                },
9113            }),
9114        }];
9115        let (edited_source, _) = frontend
9116            .edit_segments(&mock_ctx, version, sketch_id, segments)
9117            .await
9118            .unwrap();
9119
9120        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9121        assert_eq!(mock_source.text, edited_source.text);
9122
9123        mock_ctx.close().await;
9124    }
9125
9126    /// Preview segment edits should return solved geometry without persisting
9127    /// solver feedback to KCL.
9128    #[tokio::test(flavor = "multi_thread")]
9129    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9130        let initial_source = "\
9131sketch(on = XY) {
9132  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9133  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9134  fixed([line1.start, [0, 0]])
9135  coincident([line1.end, line2.start])
9136  equalLength([line1, line2])
9137}
9138";
9139
9140        let program = Program::parse(initial_source).unwrap().0.unwrap();
9141        let mut frontend = FrontendState::new();
9142        let mock_ctx = ExecutorContext::new_mock(None).await;
9143        let version = Version(0);
9144
9145        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9146        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9147        let sketch_id = sketch_object.id;
9148        let sketch = expect_sketch(sketch_object);
9149        let line2_end_id = *sketch.segments.get(4).unwrap();
9150
9151        let segments = vec![ExistingSegmentCtor {
9152            id: line2_end_id,
9153            ctor: SegmentCtor::Point(PointCtor {
9154                position: Point2d {
9155                    x: Expr::Var(Number {
9156                        value: 9.0,
9157                        units: NumericSuffix::None,
9158                    }),
9159                    y: Expr::Var(Number {
9160                        value: 10.0,
9161                        units: NumericSuffix::None,
9162                    }),
9163                },
9164            }),
9165        }];
9166        let (preview_source, preview_delta) = frontend
9167            .edit_segments_with_options(
9168                &mock_ctx,
9169                version,
9170                sketch_id,
9171                segments,
9172                EditSegmentsOptions {
9173                    anchor_segment_ids: Some(vec![line2_end_id]),
9174                    drag_anchors: Vec::new(),
9175                    constraint_label_edits: Vec::new(),
9176                    commit_solved_initial_guesses: false,
9177                },
9178            )
9179            .await
9180            .unwrap();
9181
9182        assert!(
9183            !preview_delta.exec_outcome.var_solutions.is_empty(),
9184            "preview solve should still solve and return geometry feedback"
9185        );
9186        assert!(
9187            preview_source
9188                .text
9189                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9190        );
9191        assert!(
9192            preview_source
9193                .text
9194                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9195        );
9196
9197        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9198        assert_eq!(mock_source.text, preview_source.text);
9199
9200        mock_ctx.close().await;
9201    }
9202
9203    #[tokio::test(flavor = "multi_thread")]
9204    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9205        let initial_source = "\
9206sketch(on = XY) {
9207  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9208}
9209";
9210
9211        let program = Program::parse(initial_source).unwrap().0.unwrap();
9212        let mut frontend = FrontendState::new();
9213        let mock_ctx = ExecutorContext::new_mock(None).await;
9214        let version = Version(0);
9215
9216        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9217        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9218        let sketch_id = sketch_object.id;
9219        let sketch = expect_sketch(sketch_object);
9220        let line_end_id = *sketch.segments.get(1).unwrap();
9221
9222        let constraint = Constraint::Fixed(Fixed {
9223            points: vec![FixedPoint {
9224                point: line_end_id,
9225                position: Point2d {
9226                    x: Number {
9227                        value: 20.0,
9228                        units: NumericSuffix::Mm,
9229                    },
9230                    y: Number {
9231                        value: 0.0,
9232                        units: NumericSuffix::Mm,
9233                    },
9234                },
9235            }],
9236        });
9237        let (constraint_source, _) = frontend
9238            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9239            .await
9240            .unwrap();
9241
9242        assert!(
9243            constraint_source
9244                .text
9245                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9246            "{}",
9247            constraint_source.text
9248        );
9249        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9250        assert_eq!(mock_source.text, constraint_source.text);
9251
9252        mock_ctx.close().await;
9253    }
9254
9255    #[test]
9256    fn test_no_solver_feedback_preserves_original_source() {
9257        let initial_source = "\
9258@settings(defaultLengthUnit = in, kclVersion = 2.0)
9259cylinder = startSketchOn(XY)
9260    |> circle(center= [0, 0], radius= 22)
9261    |> extrude(length = 14)
9262";
9263        let mut frontend = FrontendState::new();
9264        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9265        let outcome = ExecOutcome {
9266            variables: Default::default(),
9267            test_program_memory: Default::default(),
9268            operations: Default::default(),
9269            artifact_graph: Default::default(),
9270            scene_objects: Default::default(),
9271            source_range_to_object: Default::default(),
9272            var_solutions: Default::default(),
9273            refactor_metadata: Default::default(),
9274            issues: Default::default(),
9275            filenames: Default::default(),
9276            source_files: Default::default(),
9277            default_planes: Default::default(),
9278        };
9279
9280        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9281
9282        assert_eq!(source_delta.text, initial_source);
9283    }
9284
9285    /// Explicit drag anchors should limit which edited points become temporary
9286    /// fixed constraints.
9287    #[tokio::test(flavor = "multi_thread")]
9288    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9289        let initial_source = "\
9290sketch(on = XY) {
9291  point1 = point(at = [var 0mm, var 0mm])
9292  point2 = point(at = [var 0mm, var 0mm])
9293  coincident([point1, point2])
9294}
9295";
9296
9297        let program = Program::parse(initial_source).unwrap().0.unwrap();
9298        let mut frontend = FrontendState::new();
9299        let mock_ctx = ExecutorContext::new_mock(None).await;
9300        let version = Version(0);
9301
9302        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9304        let sketch_id = sketch_object.id;
9305        let sketch = expect_sketch(sketch_object);
9306        let point1_id = sketch.segments[0];
9307        let point2_id = sketch.segments[1];
9308
9309        let segments = vec![
9310            ExistingSegmentCtor {
9311                id: point1_id,
9312                ctor: SegmentCtor::Point(PointCtor {
9313                    position: point_expr_mm(10.0, 0.0),
9314                }),
9315            },
9316            ExistingSegmentCtor {
9317                id: point2_id,
9318                ctor: SegmentCtor::Point(PointCtor {
9319                    position: point_expr_mm(100.0, 0.0),
9320                }),
9321            },
9322        ];
9323        let (_, scene_delta) = frontend
9324            .edit_segments_with_options(
9325                &mock_ctx,
9326                version,
9327                sketch_id,
9328                segments,
9329                EditSegmentsOptions {
9330                    anchor_segment_ids: Some(vec![point1_id]),
9331                    drag_anchors: Vec::new(),
9332                    constraint_label_edits: Vec::new(),
9333                    commit_solved_initial_guesses: true,
9334                },
9335            )
9336            .await
9337            .unwrap();
9338
9339        assert_point_position_close(
9340            point_position(&scene_delta.new_graph, point1_id),
9341            point_number_mm(10.0, 0.0),
9342        );
9343        assert_point_position_close(
9344            point_position(&scene_delta.new_graph, point2_id),
9345            point_number_mm(10.0, 0.0),
9346        );
9347
9348        mock_ctx.close().await;
9349    }
9350
9351    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9352    /// for every SketchVar whose initial NumericLiteral has the given value.
9353    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9354        use std::cell::RefCell;
9355        struct Collector {
9356            target: f64,
9357            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9358        }
9359        impl<'a> crate::walk::Visitor<'a> for &Collector {
9360            type Error = crate::front::Error;
9361            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9362                if let crate::walk::Node::SketchVar(sketch_var) = node
9363                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9364                    && (initial.value - self.target).abs() < 1e-9
9365                {
9366                    self.out
9367                        .borrow_mut()
9368                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9369                }
9370                for child in node.children().iter() {
9371                    if !child.visit(*self)? {
9372                        return Ok(false);
9373                    }
9374                }
9375                Ok(true)
9376            }
9377        }
9378        let collector = Collector {
9379            target: value,
9380            out: Default::default(),
9381        };
9382        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9383        collector.out.into_inner()
9384    }
9385
9386    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9387    /// for every SketchVar (including bare `var`).
9388    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9389        use std::cell::RefCell;
9390        struct Collector {
9391            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9392        }
9393        impl<'a> crate::walk::Visitor<'a> for &Collector {
9394            type Error = crate::front::Error;
9395            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9396                if let crate::walk::Node::SketchVar(sketch_var) = node
9397                    && let Some(node_path) = &sketch_var.node_path
9398                {
9399                    self.out
9400                        .borrow_mut()
9401                        .push((SourceRange::from(sketch_var), node_path.clone()));
9402                }
9403                for child in node.children().iter() {
9404                    if !child.visit(*self)? {
9405                        return Ok(false);
9406                    }
9407                }
9408                Ok(true)
9409            }
9410        }
9411        let collector = Collector {
9412            out: Default::default(),
9413        };
9414        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9415        collector.out.into_inner()
9416    }
9417
9418    fn empty_exec_outcome_with_var_solutions(
9419        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9420    ) -> ExecOutcome {
9421        ExecOutcome {
9422            variables: Default::default(),
9423            test_program_memory: Default::default(),
9424            operations: Default::default(),
9425            artifact_graph: Default::default(),
9426            scene_objects: Default::default(),
9427            source_range_to_object: Default::default(),
9428            var_solutions,
9429            refactor_metadata: Default::default(),
9430            issues: Default::default(),
9431            filenames: Default::default(),
9432            source_files: Default::default(),
9433            default_planes: Default::default(),
9434        }
9435    }
9436
9437    /// Happy path: commit a var solution to a `var N` inside a sketch block
9438    /// using a correct NodePath. Confirms the node-path code path produces the
9439    /// expected source mutation.
9440    #[test]
9441    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9442        let initial_source = "\
9443sketch(on = XY) {
9444  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9445}
9446";
9447        let program = Program::parse(initial_source).unwrap().0.unwrap();
9448        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9449        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9450        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9451
9452        let mut frontend = FrontendState::new();
9453        frontend.program = program;
9454
9455        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9456            literal_range,
9457            Some(node_path),
9458            Number {
9459                value: 25.0,
9460                units: NumericSuffix::Mm,
9461            },
9462        )]);
9463
9464        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9465
9466        insta::assert_snapshot!(
9467            "test_commit_var_solution_by_node_path_updates_sketch_var",
9468            source_delta.text
9469        );
9470    }
9471
9472    /// Whitespace inserted earlier in the source shifts the original SketchVar
9473    /// SourceRange. With NodePath propagation the commit should still target
9474    /// the right `var`. We simulate this by collecting node_paths against a
9475    /// "compact" source, then loading the frontend with a "padded" source
9476    /// (whose byte offsets differ), and feeding the original (now stale)
9477    /// source range plus the correct node_path back into the commit.
9478    #[test]
9479    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9480        let compact_source = "\
9481sketch(on = XY) {
9482  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9483}
9484";
9485        let padded_source = "\
9486// added comment\n// added comment\n\nsketch(on = XY) {
9487  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9488}
9489";
9490        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9491        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9492
9493        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9494            .into_iter()
9495            .next()
9496            .expect("expected `var 10mm` in compact source");
9497        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9498            .into_iter()
9499            .next()
9500            .expect("expected `var 10mm` in padded source");
9501
9502        assert_ne!(
9503            compact_match.0, padded_match.0,
9504            "byte offsets must differ for this test to be meaningful"
9505        );
9506        assert_eq!(
9507            compact_match.1, padded_match.1,
9508            "node paths must agree across whitespace; that's the whole point of NodePath",
9509        );
9510
9511        let mut frontend = FrontendState::new();
9512        frontend.program = padded_program;
9513
9514        // Stale source range from the compact source + correct node_path.
9515        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9516            compact_match.0,
9517            Some(compact_match.1),
9518            Number {
9519                value: 30.0,
9520                units: NumericSuffix::Mm,
9521            },
9522        )]);
9523
9524        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9525
9526        insta::assert_snapshot!(
9527            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9528            source_delta.text
9529        );
9530    }
9531
9532    /// When multiple `var` declarations exist and the stale source range
9533    /// happens to land on a *different* var, the node_path must take
9534    /// precedence and the right var gets updated.
9535    #[test]
9536    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9537        let initial_source = "\
9538sketch(on = XY) {
9539  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9540}
9541";
9542        let program = Program::parse(initial_source).unwrap().0.unwrap();
9543
9544        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9545            .into_iter()
9546            .next()
9547            .expect("expected `var 10mm`");
9548        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9549            .into_iter()
9550            .next()
9551            .expect("expected `var 20mm`");
9552
9553        let mut frontend = FrontendState::new();
9554        frontend.program = program;
9555
9556        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9557        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9558            var_20.0,
9559            Some(var_10.1),
9560            Number {
9561                value: 33.0,
9562                units: NumericSuffix::Mm,
9563            },
9564        )]);
9565
9566        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9567
9568        insta::assert_snapshot!(
9569            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9570            source_delta.text
9571        );
9572    }
9573
9574    /// Bare `var` (no initial literal) is only locatable via node_path. With
9575    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9576    /// solver solution should fill the initial value in. The
9577    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9578    /// is gated as an experimental feature; without it the re-parse of the
9579    /// recast source rejects bare `var` declarations.
9580    #[test]
9581    fn test_commit_var_solution_writes_back_into_bare_var() {
9582        let initial_source = "\
9583@settings(experimentalFeatures = allow, kclVersion = 2.0)
9584sketch(on = XY) {
9585  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9586}
9587";
9588        let program = Program::parse(initial_source).unwrap().0.unwrap();
9589
9590        // Pick the first bare `var`; collect_all_sketch_vars returns every
9591        // SketchVar, including bare ones.
9592        let bare = collect_all_sketch_vars(&program)
9593            .into_iter()
9594            .find(|(range, _)| {
9595                // The bare `var` is exactly the 3 characters "var".
9596                range.end() - range.start() == 3
9597            })
9598            .expect("expected at least one bare `var`");
9599
9600        let mut frontend = FrontendState::new();
9601        frontend.program = program;
9602
9603        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9604            bare.0,
9605            Some(bare.1),
9606            Number {
9607                value: 7.0,
9608                units: NumericSuffix::Mm,
9609            },
9610        )]);
9611
9612        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9613
9614        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9615        // written as an explicit suffix so the bare var commits with units.
9616        // The recast adds a blank line after the `@settings` annotation.
9617        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9618    }
9619
9620    #[tokio::test(flavor = "multi_thread")]
9621    async fn test_delete_point_without_var() {
9622        let initial_source = "\
9623sketch(on = XY) {
9624  point(at = [var 1, var 2])
9625  point(at = [var 3, var 4])
9626  point(at = [var 5, var 6])
9627}
9628";
9629
9630        let program = Program::parse(initial_source).unwrap().0.unwrap();
9631
9632        let mut frontend = FrontendState::new();
9633
9634        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9635        let mock_ctx = ExecutorContext::new_mock(None).await;
9636        let version = Version(0);
9637
9638        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9639        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9640        let sketch_id = sketch_object.id;
9641        let sketch = expect_sketch(sketch_object);
9642
9643        let point_id = *sketch.segments.get(1).unwrap();
9644
9645        let (src_delta, scene_delta) = frontend
9646            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9647            .await
9648            .unwrap();
9649        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9650        assert_eq!(scene_delta.new_objects, vec![]);
9651        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9652
9653        ctx.close().await;
9654        mock_ctx.close().await;
9655    }
9656
9657    #[tokio::test(flavor = "multi_thread")]
9658    async fn test_delete_point_with_var() {
9659        let initial_source = "\
9660sketch(on = XY) {
9661  point(at = [var 1, var 2])
9662  point1 = point(at = [var 3, var 4])
9663  point(at = [var 5, var 6])
9664}
9665";
9666
9667        let program = Program::parse(initial_source).unwrap().0.unwrap();
9668
9669        let mut frontend = FrontendState::new();
9670
9671        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9672        let mock_ctx = ExecutorContext::new_mock(None).await;
9673        let version = Version(0);
9674
9675        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9676        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9677        let sketch_id = sketch_object.id;
9678        let sketch = expect_sketch(sketch_object);
9679
9680        let point_id = *sketch.segments.get(1).unwrap();
9681
9682        let (src_delta, scene_delta) = frontend
9683            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9684            .await
9685            .unwrap();
9686        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9687        assert_eq!(scene_delta.new_objects, vec![]);
9688        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9689
9690        ctx.close().await;
9691        mock_ctx.close().await;
9692    }
9693
9694    #[tokio::test(flavor = "multi_thread")]
9695    async fn test_delete_multiple_points() {
9696        let initial_source = "\
9697sketch(on = XY) {
9698  point(at = [var 1, var 2])
9699  point1 = point(at = [var 3, var 4])
9700  point(at = [var 5, var 6])
9701}
9702";
9703
9704        let program = Program::parse(initial_source).unwrap().0.unwrap();
9705
9706        let mut frontend = FrontendState::new();
9707
9708        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9709        let mock_ctx = ExecutorContext::new_mock(None).await;
9710        let version = Version(0);
9711
9712        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9713        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9714        let sketch_id = sketch_object.id;
9715
9716        let sketch = expect_sketch(sketch_object);
9717
9718        let point1_id = *sketch.segments.first().unwrap();
9719        let point2_id = *sketch.segments.get(1).unwrap();
9720
9721        let (src_delta, scene_delta) = frontend
9722            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9723            .await
9724            .unwrap();
9725        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9726        assert_eq!(scene_delta.new_objects, vec![]);
9727        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9728
9729        ctx.close().await;
9730        mock_ctx.close().await;
9731    }
9732
9733    #[tokio::test(flavor = "multi_thread")]
9734    async fn test_delete_coincident_constraint() {
9735        let initial_source = "\
9736sketch(on = XY) {
9737  point1 = point(at = [var 1, var 2])
9738  point2 = point(at = [var 3, var 4])
9739  coincident([point1, point2])
9740  point(at = [var 5, var 6])
9741}
9742";
9743
9744        let program = Program::parse(initial_source).unwrap().0.unwrap();
9745
9746        let mut frontend = FrontendState::new();
9747
9748        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9749        let mock_ctx = ExecutorContext::new_mock(None).await;
9750        let version = Version(0);
9751
9752        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9753        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9754        let sketch_id = sketch_object.id;
9755        let sketch = expect_sketch(sketch_object);
9756
9757        let coincident_id = *sketch.constraints.first().unwrap();
9758
9759        let (src_delta, scene_delta) = frontend
9760            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9761            .await
9762            .unwrap();
9763        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9764        assert_eq!(scene_delta.new_objects, vec![]);
9765        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9766
9767        ctx.close().await;
9768        mock_ctx.close().await;
9769    }
9770
9771    #[tokio::test(flavor = "multi_thread")]
9772    async fn test_delete_line_cascades_to_coincident_constraint() {
9773        let initial_source = "\
9774sketch(on = XY) {
9775  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9776  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9777  coincident([line1.end, line2.start])
9778}
9779";
9780
9781        let program = Program::parse(initial_source).unwrap().0.unwrap();
9782
9783        let mut frontend = FrontendState::new();
9784
9785        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9786        let mock_ctx = ExecutorContext::new_mock(None).await;
9787        let version = Version(0);
9788
9789        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9790        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9791        let sketch_id = sketch_object.id;
9792        let sketch = expect_sketch(sketch_object);
9793        let line_id = *sketch.segments.get(5).unwrap();
9794
9795        let (src_delta, scene_delta) = frontend
9796            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9797            .await
9798            .unwrap();
9799        insta::assert_snapshot!(
9800            "test_delete_line_cascades_to_coincident_constraint",
9801            src_delta.text.as_str()
9802        );
9803        assert_eq!(
9804            scene_delta.new_graph.objects.len(),
9805            5,
9806            "{:#?}",
9807            scene_delta.new_graph.objects
9808        );
9809
9810        ctx.close().await;
9811        mock_ctx.close().await;
9812    }
9813
9814    #[tokio::test(flavor = "multi_thread")]
9815    async fn test_delete_line_cascades_to_distance_constraint() {
9816        let initial_source = "\
9817sketch(on = XY) {
9818  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9819  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9820  distance([line1.end, line2.start]) == 10mm
9821}
9822";
9823
9824        let program = Program::parse(initial_source).unwrap().0.unwrap();
9825
9826        let mut frontend = FrontendState::new();
9827
9828        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9829        let mock_ctx = ExecutorContext::new_mock(None).await;
9830        let version = Version(0);
9831
9832        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9834        let sketch_id = sketch_object.id;
9835        let sketch = expect_sketch(sketch_object);
9836        let line_id = *sketch.segments.get(5).unwrap();
9837
9838        let (src_delta, scene_delta) = frontend
9839            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9840            .await
9841            .unwrap();
9842        insta::assert_snapshot!(
9843            "test_delete_line_cascades_to_distance_constraint",
9844            src_delta.text.as_str()
9845        );
9846        assert_eq!(
9847            scene_delta.new_graph.objects.len(),
9848            5,
9849            "{:#?}",
9850            scene_delta.new_graph.objects
9851        );
9852
9853        ctx.close().await;
9854        mock_ctx.close().await;
9855    }
9856
9857    #[tokio::test(flavor = "multi_thread")]
9858    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9859        let initial_source = "\
9860sketch(on = XY) {
9861  point1 = point(at = [var 1, var 2])
9862  point2 = point(at = [var 3, var 4])
9863  horizontalDistance([point1, point2]) == 10mm
9864}
9865";
9866
9867        let program = Program::parse(initial_source).unwrap().0.unwrap();
9868
9869        let mut frontend = FrontendState::new();
9870
9871        let mock_ctx = ExecutorContext::new_mock(None).await;
9872        let version = Version(0);
9873
9874        frontend.program = program.clone();
9875        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9876        frontend.update_state_after_exec(outcome, true);
9877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878        let sketch_id = sketch_object.id;
9879        let sketch = expect_sketch(sketch_object);
9880        let point2_id = *sketch.segments.get(1).unwrap();
9881
9882        let (src_delta, scene_delta) = frontend
9883            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9884            .await
9885            .unwrap();
9886        insta::assert_snapshot!(
9887            "test_delete_point_cascades_to_horizontal_distance_constraint",
9888            src_delta.text.as_str()
9889        );
9890        assert_eq!(
9891            scene_delta.new_graph.objects.len(),
9892            3,
9893            "{:#?}",
9894            scene_delta.new_graph.objects
9895        );
9896
9897        mock_ctx.close().await;
9898    }
9899
9900    #[tokio::test(flavor = "multi_thread")]
9901    async fn test_delete_line_cascades_to_fixed_constraint() {
9902        let initial_source = "\
9903sketch(on = XY) {
9904  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9905  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9906  fixed([line1.start, [0, 0]])
9907}
9908";
9909
9910        let program = Program::parse(initial_source).unwrap().0.unwrap();
9911
9912        let mut frontend = FrontendState::new();
9913
9914        let mock_ctx = ExecutorContext::new_mock(None).await;
9915        let version = Version(0);
9916
9917        frontend.program = program.clone();
9918        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9919        frontend.update_state_after_exec(outcome, true);
9920        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9921        let sketch_id = sketch_object.id;
9922        let sketch = expect_sketch(sketch_object);
9923        let line1_id = *sketch.segments.get(2).unwrap();
9924
9925        let (src_delta, scene_delta) = frontend
9926            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9927            .await
9928            .unwrap();
9929        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9930        assert_eq!(
9931            scene_delta.new_graph.objects.len(),
9932            5,
9933            "{:#?}",
9934            scene_delta.new_graph.objects
9935        );
9936
9937        mock_ctx.close().await;
9938    }
9939
9940    #[tokio::test(flavor = "multi_thread")]
9941    async fn test_delete_line_cascades_to_midpoint_constraint() {
9942        let initial_source = "\
9943sketch(on = XY) {
9944  point1 = point(at = [var 1, var 2])
9945  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9946  midpoint(line1, point = point1)
9947}
9948";
9949
9950        let program = Program::parse(initial_source).unwrap().0.unwrap();
9951
9952        let mut frontend = FrontendState::new();
9953
9954        let mock_ctx = ExecutorContext::new_mock(None).await;
9955        let version = Version(0);
9956
9957        frontend.program = program.clone();
9958        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9959        frontend.update_state_after_exec(outcome, true);
9960        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961        let sketch_id = sketch_object.id;
9962        let sketch = expect_sketch(sketch_object);
9963        let line1_id = *sketch.segments.get(3).unwrap();
9964
9965        let (src_delta, scene_delta) = frontend
9966            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9967            .await
9968            .unwrap();
9969        insta::assert_snapshot!(
9970            "test_delete_line_cascades_to_midpoint_constraint",
9971            src_delta.text.as_str()
9972        );
9973        assert_eq!(
9974            scene_delta.new_graph.objects.len(),
9975            3,
9976            "{:#?}",
9977            scene_delta.new_graph.objects
9978        );
9979
9980        mock_ctx.close().await;
9981    }
9982
9983    #[tokio::test(flavor = "multi_thread")]
9984    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9985        let initial_source = "\
9986sketch(on = XY) {
9987  point1 = point(at = [var 1, var 2])
9988  point2 = point(at = [var 3, var 4])
9989  point3 = point(at = [var 5, var 6])
9990  coincident([point1, point2, point3])
9991}
9992";
9993
9994        let program = Program::parse(initial_source).unwrap().0.unwrap();
9995
9996        let mut frontend = FrontendState::new();
9997
9998        let mock_ctx = ExecutorContext::new_mock(None).await;
9999        let version = Version(0);
10000
10001        frontend.program = program.clone();
10002        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10003        frontend.update_state_after_exec(outcome, true);
10004        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10005        let sketch_id = sketch_object.id;
10006        let sketch = expect_sketch(sketch_object);
10007        let point3_id = *sketch.segments.get(2).unwrap();
10008
10009        let (src_delta, scene_delta) = frontend
10010            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10011            .await
10012            .unwrap();
10013        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10014        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10015        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10016        assert!(
10017            src_delta.text.contains("coincident([point1, point2])"),
10018            "{}",
10019            src_delta.text
10020        );
10021
10022        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10023        let sketch = expect_sketch(sketch_object);
10024        assert_eq!(sketch.segments.len(), 2);
10025        assert_eq!(sketch.constraints.len(), 1);
10026
10027        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10028        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10029            panic!("Expected constraint object");
10030        };
10031        let Constraint::Coincident(coincident) = constraint else {
10032            panic!("Expected coincident constraint");
10033        };
10034        assert_eq!(
10035            coincident.segments,
10036            sketch
10037                .segments
10038                .iter()
10039                .copied()
10040                .map(Into::into)
10041                .collect::<Vec<ConstraintSegment>>()
10042        );
10043
10044        mock_ctx.close().await;
10045    }
10046
10047    #[tokio::test(flavor = "multi_thread")]
10048    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10049        let initial_source = "\
10050sketch(on = XY) {
10051  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10052  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10053  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10054  equalLength([line1, line2, line3])
10055}
10056";
10057
10058        let program = Program::parse(initial_source).unwrap().0.unwrap();
10059
10060        let mut frontend = FrontendState::new();
10061
10062        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10063        let mock_ctx = ExecutorContext::new_mock(None).await;
10064        let version = Version(0);
10065
10066        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10067        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10068        let sketch_id = sketch_object.id;
10069        let sketch = expect_sketch(sketch_object);
10070        let line3_id = *sketch.segments.get(8).unwrap();
10071
10072        let (src_delta, scene_delta) = frontend
10073            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10074            .await
10075            .unwrap();
10076        insta::assert_snapshot!(
10077            "test_delete_line_preserves_multiline_equal_length_constraint",
10078            src_delta.text.as_str()
10079        );
10080
10081        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10082        let sketch = expect_sketch(sketch_object);
10083        assert_eq!(sketch.constraints.len(), 1);
10084
10085        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10086        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10087            panic!("Expected constraint object");
10088        };
10089        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10090            panic!("Expected lines equal length constraint");
10091        };
10092        assert_eq!(lines_equal_length.lines.len(), 2);
10093
10094        ctx.close().await;
10095        mock_ctx.close().await;
10096    }
10097
10098    #[tokio::test(flavor = "multi_thread")]
10099    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10100        let initial_source = "\
10101sketch(on = XY) {
10102  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10103  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10104  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10105  horizontal([line1.end, line2.start, line3.start])
10106}
10107";
10108
10109        let program = Program::parse(initial_source).unwrap().0.unwrap();
10110
10111        let mut frontend = FrontendState::new();
10112
10113        let mock_ctx = ExecutorContext::new_mock(None).await;
10114        let version = Version(0);
10115
10116        frontend.program = program.clone();
10117        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10118        frontend.update_state_after_exec(outcome, true);
10119        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10120        let sketch_id = sketch_object.id;
10121        let sketch = expect_sketch(sketch_object);
10122        let line1_id = *sketch.segments.get(2).unwrap();
10123
10124        let (src_delta, scene_delta) = frontend
10125            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10126            .await
10127            .unwrap();
10128        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10129        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10130        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10131        assert!(
10132            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10133            "{}",
10134            src_delta.text
10135        );
10136
10137        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10138        let sketch = expect_sketch(sketch_object);
10139        assert_eq!(sketch.constraints.len(), 1);
10140
10141        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10142        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10143            panic!("Expected constraint object");
10144        };
10145        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10146            panic!("Expected horizontal points constraint");
10147        };
10148        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10149        assert_eq!(*points, remaining_points);
10150
10151        mock_ctx.close().await;
10152    }
10153
10154    #[tokio::test(flavor = "multi_thread")]
10155    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10156        let initial_source = "\
10157sketch(on = XY) {
10158  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10159  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10160  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10161  vertical([line1.end, line2.start, line3.start])
10162}
10163";
10164
10165        let program = Program::parse(initial_source).unwrap().0.unwrap();
10166
10167        let mut frontend = FrontendState::new();
10168
10169        let mock_ctx = ExecutorContext::new_mock(None).await;
10170        let version = Version(0);
10171
10172        frontend.program = program.clone();
10173        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10174        frontend.update_state_after_exec(outcome, true);
10175        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10176        let sketch_id = sketch_object.id;
10177        let sketch = expect_sketch(sketch_object);
10178        let line1_id = *sketch.segments.get(2).unwrap();
10179
10180        let (src_delta, scene_delta) = frontend
10181            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10182            .await
10183            .unwrap();
10184        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10185        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10186        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10187        assert!(
10188            src_delta.text.contains("vertical([line2.start, line3.start])"),
10189            "{}",
10190            src_delta.text
10191        );
10192
10193        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10194        let sketch = expect_sketch(sketch_object);
10195        assert_eq!(sketch.constraints.len(), 1);
10196
10197        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10198        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10199            panic!("Expected constraint object");
10200        };
10201        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10202            panic!("Expected vertical points constraint");
10203        };
10204        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10205        assert_eq!(*points, remaining_points);
10206
10207        mock_ctx.close().await;
10208    }
10209
10210    #[tokio::test(flavor = "multi_thread")]
10211    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10212        let initial_source = "\
10213sketch(on = XY) {
10214  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10215  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10216  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10217  coincident([line1.end, line2.start, line3.start])
10218}
10219";
10220
10221        let program = Program::parse(initial_source).unwrap().0.unwrap();
10222
10223        let mut frontend = FrontendState::new();
10224
10225        let mock_ctx = ExecutorContext::new_mock(None).await;
10226        let version = Version(0);
10227
10228        frontend.program = program.clone();
10229        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10230        frontend.update_state_after_exec(outcome, true);
10231        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10232        let sketch_id = sketch_object.id;
10233        let sketch = expect_sketch(sketch_object);
10234        let line1_id = *sketch.segments.get(2).unwrap();
10235
10236        let (src_delta, scene_delta) = frontend
10237            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10238            .await
10239            .unwrap();
10240        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10241        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10242        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10243        assert!(
10244            src_delta.text.contains("coincident([line2.start, line3.start])"),
10245            "{}",
10246            src_delta.text
10247        );
10248
10249        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10250        let sketch = expect_sketch(sketch_object);
10251        assert_eq!(sketch.constraints.len(), 1);
10252
10253        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10254        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10255            panic!("Expected constraint object");
10256        };
10257        let Constraint::Coincident(coincident) = constraint else {
10258            panic!("Expected coincident constraint");
10259        };
10260        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10261        assert_eq!(coincident.segments, remaining_segments);
10262
10263        mock_ctx.close().await;
10264    }
10265
10266    #[tokio::test(flavor = "multi_thread")]
10267    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10268        let initial_source = "\
10269sketch(on = XY) {
10270  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10271  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10272  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10273  equalLength([line1, line2, line3])
10274}
10275";
10276
10277        let program = Program::parse(initial_source).unwrap().0.unwrap();
10278
10279        let mut frontend = FrontendState::new();
10280
10281        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10282        let mock_ctx = ExecutorContext::new_mock(None).await;
10283        let version = Version(0);
10284
10285        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10286        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10287        let sketch_id = sketch_object.id;
10288        let sketch = expect_sketch(sketch_object);
10289        let line2_id = *sketch.segments.get(5).unwrap();
10290        let line3_id = *sketch.segments.get(8).unwrap();
10291
10292        let (src_delta, scene_delta) = frontend
10293            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10294            .await
10295            .unwrap();
10296        insta::assert_snapshot!(
10297            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10298            src_delta.text.as_str()
10299        );
10300
10301        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10302        let sketch = expect_sketch(sketch_object);
10303        assert!(sketch.constraints.is_empty());
10304
10305        ctx.close().await;
10306        mock_ctx.close().await;
10307    }
10308
10309    #[tokio::test(flavor = "multi_thread")]
10310    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10311        let initial_source = "\
10312sketch(on = XY) {
10313  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10314  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10315  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10316  parallel([line1, line2, line3])
10317}
10318";
10319
10320        let program = Program::parse(initial_source).unwrap().0.unwrap();
10321
10322        let mut frontend = FrontendState::new();
10323
10324        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10325        let mock_ctx = ExecutorContext::new_mock(None).await;
10326        let version = Version(0);
10327
10328        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10329        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10330        let sketch_id = sketch_object.id;
10331        let sketch = expect_sketch(sketch_object);
10332        let line3_id = *sketch.segments.get(8).unwrap();
10333
10334        let (src_delta, scene_delta) = frontend
10335            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10336            .await
10337            .unwrap();
10338        insta::assert_snapshot!(
10339            "test_delete_line_preserves_multiline_parallel_constraint",
10340            src_delta.text.as_str()
10341        );
10342
10343        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10344        let sketch = expect_sketch(sketch_object);
10345        assert_eq!(sketch.constraints.len(), 1);
10346
10347        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10348        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10349            panic!("Expected constraint object");
10350        };
10351        let Constraint::Parallel(parallel) = constraint else {
10352            panic!("Expected parallel constraint");
10353        };
10354        assert_eq!(parallel.lines.len(), 2);
10355
10356        ctx.close().await;
10357        mock_ctx.close().await;
10358    }
10359
10360    #[tokio::test(flavor = "multi_thread")]
10361    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10362        let initial_source = "\
10363sketch(on = XY) {
10364  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10365  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10366  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10367  parallel([line1, line2, line3])
10368}
10369";
10370
10371        let program = Program::parse(initial_source).unwrap().0.unwrap();
10372
10373        let mut frontend = FrontendState::new();
10374
10375        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10376        let mock_ctx = ExecutorContext::new_mock(None).await;
10377        let version = Version(0);
10378
10379        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10380        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10381        let sketch_id = sketch_object.id;
10382        let sketch = expect_sketch(sketch_object);
10383        let line2_id = *sketch.segments.get(5).unwrap();
10384        let line3_id = *sketch.segments.get(8).unwrap();
10385
10386        let (src_delta, scene_delta) = frontend
10387            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10388            .await
10389            .unwrap();
10390        insta::assert_snapshot!(
10391            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10392            src_delta.text.as_str()
10393        );
10394
10395        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10396        let sketch = expect_sketch(sketch_object);
10397        assert!(sketch.constraints.is_empty());
10398
10399        ctx.close().await;
10400        mock_ctx.close().await;
10401    }
10402
10403    #[tokio::test(flavor = "multi_thread")]
10404    async fn test_delete_line_line_coincident_constraint() {
10405        let initial_source = "\
10406sketch(on = XY) {
10407  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10408  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10409  coincident([line1, line2])
10410}
10411";
10412
10413        let program = Program::parse(initial_source).unwrap().0.unwrap();
10414
10415        let mut frontend = FrontendState::new();
10416
10417        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10418        let mock_ctx = ExecutorContext::new_mock(None).await;
10419        let version = Version(0);
10420
10421        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10422        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10423        let sketch_id = sketch_object.id;
10424        let sketch = expect_sketch(sketch_object);
10425
10426        let coincident_id = *sketch.constraints.first().unwrap();
10427
10428        let (src_delta, scene_delta) = frontend
10429            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10430            .await
10431            .unwrap();
10432        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10433        assert_eq!(scene_delta.new_objects, vec![]);
10434        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10435
10436        ctx.close().await;
10437        mock_ctx.close().await;
10438    }
10439
10440    #[tokio::test(flavor = "multi_thread")]
10441    async fn test_two_points_coincident() {
10442        let initial_source = "\
10443sketch(on = XY) {
10444  point1 = point(at = [var 1, var 2])
10445  point(at = [3, 4])
10446}
10447";
10448
10449        let program = Program::parse(initial_source).unwrap().0.unwrap();
10450
10451        let mut frontend = FrontendState::new();
10452
10453        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10454        let mock_ctx = ExecutorContext::new_mock(None).await;
10455        let version = Version(0);
10456
10457        frontend.hack_set_program(&ctx, program).await.unwrap();
10458        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10459        let sketch_id = sketch_object.id;
10460        let sketch = expect_sketch(sketch_object);
10461        let point0_id = *sketch.segments.first().unwrap();
10462        let point1_id = *sketch.segments.get(1).unwrap();
10463
10464        let constraint = Constraint::Coincident(Coincident {
10465            segments: vec![point0_id.into(), point1_id.into()],
10466        });
10467        let (src_delta, scene_delta) = frontend
10468            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10469            .await
10470            .unwrap();
10471        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10472        assert_eq!(
10473            scene_delta.new_graph.objects.len(),
10474            5,
10475            "{:#?}",
10476            scene_delta.new_graph.objects
10477        );
10478
10479        ctx.close().await;
10480        mock_ctx.close().await;
10481    }
10482
10483    #[tokio::test(flavor = "multi_thread")]
10484    async fn test_three_points_coincident() {
10485        let initial_source = "\
10486sketch(on = XY) {
10487  point1 = point(at = [var 1, var 2])
10488  point(at = [var 3, var 4])
10489  point(at = [var 5, var 6])
10490}
10491";
10492
10493        let program = Program::parse(initial_source).unwrap().0.unwrap();
10494
10495        let mut frontend = FrontendState::new();
10496
10497        let mock_ctx = ExecutorContext::new_mock(None).await;
10498        let version = Version(0);
10499
10500        frontend.program = program.clone();
10501        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10502        frontend.update_state_after_exec(outcome, true);
10503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10504        let sketch_id = sketch_object.id;
10505        let sketch = expect_sketch(sketch_object);
10506        let segments = sketch
10507            .segments
10508            .iter()
10509            .take(3)
10510            .copied()
10511            .map(Into::into)
10512            .collect::<Vec<ConstraintSegment>>();
10513
10514        let constraint = Constraint::Coincident(Coincident {
10515            segments: segments.clone(),
10516        });
10517        let (src_delta, scene_delta) = frontend
10518            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10519            .await
10520            .unwrap();
10521        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10522
10523        let constraint_object = scene_delta
10524            .new_graph
10525            .objects
10526            .iter()
10527            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10528            .unwrap();
10529
10530        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10531            panic!("expected a constraint object");
10532        };
10533
10534        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10535
10536        mock_ctx.close().await;
10537    }
10538
10539    #[tokio::test(flavor = "multi_thread")]
10540    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10541        let initial_source = "\
10542sketch(on = XY) {
10543  point1 = point(at = [var 1, var 2])
10544  point2 = point(at = [var 3, var 4])
10545  point3 = point(at = [var 5, var 6])
10546  coincident([point1, point2, point3])
10547}
10548";
10549
10550        let program = Program::parse(initial_source).unwrap().0.unwrap();
10551
10552        let mut frontend = FrontendState::new();
10553
10554        let ctx = ExecutorContext::new_mock(None).await;
10555        frontend.program = program.clone();
10556        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10557        frontend.update_state_after_exec(outcome, true);
10558
10559        let constraint_object = frontend
10560            .scene_graph
10561            .objects
10562            .iter()
10563            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10564            .unwrap();
10565        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10566            panic!("expected a constraint object");
10567        };
10568
10569        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10570        let sketch = expect_sketch(sketch_object);
10571        let expected_segments = sketch
10572            .segments
10573            .iter()
10574            .take(3)
10575            .copied()
10576            .map(Into::into)
10577            .collect::<Vec<ConstraintSegment>>();
10578
10579        assert_eq!(
10580            constraint,
10581            &Constraint::Coincident(Coincident {
10582                segments: expected_segments,
10583            })
10584        );
10585
10586        ctx.close().await;
10587    }
10588
10589    #[tokio::test(flavor = "multi_thread")]
10590    async fn test_point_origin_coincident_preserves_order() {
10591        let initial_source = "\
10592sketch(on = XY) {
10593  point(at = [var 1, var 2])
10594}
10595";
10596
10597        for (origin_first, snapshot_name) in [
10598            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10599            (false, "test_point_origin_coincident_preserves_order_point_first"),
10600        ] {
10601            let program = Program::parse(initial_source).unwrap().0.unwrap();
10602
10603            let mut frontend = FrontendState::new();
10604
10605            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10606            let mock_ctx = ExecutorContext::new_mock(None).await;
10607            let version = Version(0);
10608
10609            frontend.hack_set_program(&ctx, program).await.unwrap();
10610            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10611            let sketch_id = sketch_object.id;
10612            let sketch = expect_sketch(sketch_object);
10613            let point_id = *sketch.segments.first().unwrap();
10614
10615            let segments = if origin_first {
10616                vec![ConstraintSegment::ORIGIN, point_id.into()]
10617            } else {
10618                vec![point_id.into(), ConstraintSegment::ORIGIN]
10619            };
10620            let constraint = Constraint::Coincident(Coincident {
10621                segments: segments.clone(),
10622            });
10623            let (src_delta, scene_delta) = frontend
10624                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10625                .await
10626                .unwrap();
10627            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10628
10629            let constraint_object = scene_delta
10630                .new_graph
10631                .objects
10632                .iter()
10633                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10634                .unwrap();
10635
10636            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10637                panic!("expected a constraint object");
10638            };
10639
10640            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10641
10642            ctx.close().await;
10643            mock_ctx.close().await;
10644        }
10645    }
10646
10647    #[tokio::test(flavor = "multi_thread")]
10648    async fn test_coincident_of_line_end_points() {
10649        let initial_source = "\
10650sketch(on = XY) {
10651  line(start = [var 1, var 2], end = [var 3, var 4])
10652  line(start = [var 5, var 6], end = [var 7, var 8])
10653}
10654";
10655
10656        let program = Program::parse(initial_source).unwrap().0.unwrap();
10657
10658        let mut frontend = FrontendState::new();
10659
10660        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10661        let mock_ctx = ExecutorContext::new_mock(None).await;
10662        let version = Version(0);
10663
10664        frontend.hack_set_program(&ctx, program).await.unwrap();
10665        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10666        let sketch_id = sketch_object.id;
10667        let sketch = expect_sketch(sketch_object);
10668        let point0_id = *sketch.segments.get(1).unwrap();
10669        let point1_id = *sketch.segments.get(3).unwrap();
10670
10671        let constraint = Constraint::Coincident(Coincident {
10672            segments: vec![point0_id.into(), point1_id.into()],
10673        });
10674        let (src_delta, scene_delta) = frontend
10675            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10676            .await
10677            .unwrap();
10678        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10679        assert_eq!(
10680            scene_delta.new_graph.objects.len(),
10681            9,
10682            "{:#?}",
10683            scene_delta.new_graph.objects
10684        );
10685
10686        ctx.close().await;
10687        mock_ctx.close().await;
10688    }
10689
10690    #[tokio::test(flavor = "multi_thread")]
10691    async fn test_coincident_of_line_point_and_circle_segment() {
10692        let initial_source = "\
10693sketch(on = XY) {
10694  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10695  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10696}
10697";
10698        let program = Program::parse(initial_source).unwrap().0.unwrap();
10699        let mut frontend = FrontendState::new();
10700
10701        let mock_ctx = ExecutorContext::new_mock(None).await;
10702        let version = Version(0);
10703
10704        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10705        frontend.program = program;
10706        frontend.update_state_after_exec(outcome, true);
10707        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10708        let sketch_id = sketch_object.id;
10709        let sketch = expect_sketch(sketch_object);
10710
10711        let circle_id = sketch
10712            .segments
10713            .iter()
10714            .copied()
10715            .find(|seg_id| {
10716                matches!(
10717                    &frontend.scene_graph.objects[seg_id.0].kind,
10718                    ObjectKind::Segment {
10719                        segment: Segment::Circle(_)
10720                    }
10721                )
10722            })
10723            .expect("Expected a circle segment in sketch");
10724        let line_id = frontend
10725            .scene_graph
10726            .objects
10727            .iter()
10728            .find_map(|obj| match &obj.kind {
10729                ObjectKind::Segment {
10730                    segment: Segment::Line(line),
10731                } if line.owner.is_none() => Some(obj.id),
10732                _ => None,
10733            })
10734            .expect("Expected a standalone line segment in scene graph");
10735
10736        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10737            ObjectKind::Segment {
10738                segment: Segment::Line(line),
10739            } => line.start,
10740            _ => panic!("Expected line segment object"),
10741        };
10742
10743        let constraint = Constraint::Coincident(Coincident {
10744            segments: vec![line_start_point_id.into(), circle_id.into()],
10745        });
10746        let (src_delta, _scene_delta) = frontend
10747            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10748            .await
10749            .unwrap();
10750        insta::assert_snapshot!(
10751            "test_coincident_of_line_point_and_circle_segment",
10752            src_delta.text.as_str()
10753        );
10754
10755        mock_ctx.close().await;
10756    }
10757
10758    #[tokio::test(flavor = "multi_thread")]
10759    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10760        // Test that attempting an invalid coincident constraint (arc and line)
10761        // doesn't corrupt the state, allowing subsequent operations to work.
10762        // This test verifies the transactional fix in add_constraint that prevents
10763        // state corruption when invalid constraints are attempted.
10764        // Example: coincident constraint between an arc segment and a straight line segment
10765        // is geometrically invalid and should fail, but state should remain intact.
10766        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10767        let program = Program::empty();
10768
10769        let mut frontend = FrontendState::new();
10770        frontend.program = program;
10771
10772        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10773        let mock_ctx = ExecutorContext::new_mock(None).await;
10774        let version = Version(0);
10775
10776        let sketch_args = SketchCtor {
10777            on: Plane::Default(PlaneName::Xy),
10778        };
10779        let (_src_delta, _scene_delta, sketch_id) = frontend
10780            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10781            .await
10782            .unwrap();
10783
10784        // Add an arc segment
10785        let arc_ctor = ArcCtor {
10786            start: Point2d {
10787                x: Expr::Var(Number {
10788                    value: 0.0,
10789                    units: NumericSuffix::Mm,
10790                }),
10791                y: Expr::Var(Number {
10792                    value: 0.0,
10793                    units: NumericSuffix::Mm,
10794                }),
10795            },
10796            end: Point2d {
10797                x: Expr::Var(Number {
10798                    value: 10.0,
10799                    units: NumericSuffix::Mm,
10800                }),
10801                y: Expr::Var(Number {
10802                    value: 10.0,
10803                    units: NumericSuffix::Mm,
10804                }),
10805            },
10806            center: Point2d {
10807                x: Expr::Var(Number {
10808                    value: 10.0,
10809                    units: NumericSuffix::Mm,
10810                }),
10811                y: Expr::Var(Number {
10812                    value: 0.0,
10813                    units: NumericSuffix::Mm,
10814                }),
10815            },
10816            direction: None,
10817            construction: None,
10818        };
10819        let (_src_delta, scene_delta) = frontend
10820            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10821            .await
10822            .unwrap();
10823        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10824        let arc_id = *scene_delta.new_objects.last().unwrap();
10825
10826        // Add a line segment
10827        let line_ctor = LineCtor {
10828            start: Point2d {
10829                x: Expr::Var(Number {
10830                    value: 20.0,
10831                    units: NumericSuffix::Mm,
10832                }),
10833                y: Expr::Var(Number {
10834                    value: 0.0,
10835                    units: NumericSuffix::Mm,
10836                }),
10837            },
10838            end: Point2d {
10839                x: Expr::Var(Number {
10840                    value: 30.0,
10841                    units: NumericSuffix::Mm,
10842                }),
10843                y: Expr::Var(Number {
10844                    value: 10.0,
10845                    units: NumericSuffix::Mm,
10846                }),
10847            },
10848            construction: None,
10849        };
10850        let (_src_delta, scene_delta) = frontend
10851            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10852            .await
10853            .unwrap();
10854        // The line is the last object in new_objects (after the 2 points: start, end)
10855        let line_id = *scene_delta.new_objects.last().unwrap();
10856
10857        // Attempt to add an invalid coincident constraint between arc and line
10858        // This should fail during execution, but state should remain intact
10859        let constraint = Constraint::Coincident(Coincident {
10860            segments: vec![arc_id.into(), line_id.into()],
10861        });
10862        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10863
10864        // The constraint addition should fail (invalid constraint)
10865        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10866
10867        // Verify state is not corrupted by checking that we can still access the scene graph
10868        // and that the original segments are still present with their source ranges
10869        let sketch_object_after =
10870            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10871        let sketch_after = expect_sketch(sketch_object_after);
10872
10873        // Verify both segments are still in the sketch
10874        assert!(
10875            sketch_after.segments.contains(&arc_id),
10876            "Arc segment should still exist after failed constraint"
10877        );
10878        assert!(
10879            sketch_after.segments.contains(&line_id),
10880            "Line segment should still exist after failed constraint"
10881        );
10882
10883        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10884        let arc_obj = frontend
10885            .scene_graph
10886            .objects
10887            .get(arc_id.0)
10888            .expect("Arc object should still be accessible");
10889        let line_obj = frontend
10890            .scene_graph
10891            .objects
10892            .get(line_id.0)
10893            .expect("Line object should still be accessible");
10894
10895        // Verify source ranges are still valid (not corrupted)
10896        // Just verify that the objects are still accessible and have the expected types
10897        match &arc_obj.kind {
10898            ObjectKind::Segment {
10899                segment: Segment::Arc(_),
10900            } => {}
10901            _ => panic!("Arc object should still be an arc segment"),
10902        }
10903        match &line_obj.kind {
10904            ObjectKind::Segment {
10905                segment: Segment::Line(_),
10906            } => {}
10907            _ => panic!("Line object should still be a line segment"),
10908        }
10909
10910        ctx.close().await;
10911        mock_ctx.close().await;
10912    }
10913
10914    #[tokio::test(flavor = "multi_thread")]
10915    async fn test_distance_two_points() {
10916        let initial_source = "\
10917sketch(on = XY) {
10918  point(at = [var 1, var 2])
10919  point(at = [var 3, var 4])
10920}
10921";
10922
10923        let program = Program::parse(initial_source).unwrap().0.unwrap();
10924
10925        let mut frontend = FrontendState::new();
10926
10927        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10928        let mock_ctx = ExecutorContext::new_mock(None).await;
10929        let version = Version(0);
10930
10931        frontend.hack_set_program(&ctx, program).await.unwrap();
10932        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10933        let sketch_id = sketch_object.id;
10934        let sketch = expect_sketch(sketch_object);
10935        let point0_id = *sketch.segments.first().unwrap();
10936        let point1_id = *sketch.segments.get(1).unwrap();
10937
10938        let constraint = Constraint::Distance(Distance {
10939            segments: vec![point0_id.into(), point1_id.into()],
10940            distance: Number {
10941                value: 2.0,
10942                units: NumericSuffix::Mm,
10943            },
10944            label_position: None,
10945            source: Default::default(),
10946        });
10947        let (src_delta, scene_delta) = frontend
10948            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10949            .await
10950            .unwrap();
10951        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10952        assert_eq!(
10953            scene_delta.new_graph.objects.len(),
10954            5,
10955            "{:#?}",
10956            scene_delta.new_graph.objects
10957        );
10958
10959        ctx.close().await;
10960        mock_ctx.close().await;
10961    }
10962
10963    #[tokio::test(flavor = "multi_thread")]
10964    async fn test_distance_two_points_with_label() {
10965        let initial_source = "\
10966sketch(on = XY) {
10967  point(at = [var 1, var 2])
10968  point(at = [var 3, var 4])
10969}
10970";
10971
10972        let program = Program::parse(initial_source).unwrap().0.unwrap();
10973
10974        let mut frontend = FrontendState::new();
10975
10976        let mock_ctx = ExecutorContext::new_mock(None).await;
10977        let version = Version(0);
10978
10979        frontend.program = program.clone();
10980        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10981        frontend.update_state_after_exec(outcome, true);
10982        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10983        let sketch_id = sketch_object.id;
10984        let sketch = expect_sketch(sketch_object);
10985        let point0_id = *sketch.segments.first().unwrap();
10986        let point1_id = *sketch.segments.get(1).unwrap();
10987
10988        let label_position = Point2d {
10989            x: Number {
10990                value: 10.0,
10991                units: NumericSuffix::Mm,
10992            },
10993            y: Number {
10994                value: 11.0,
10995                units: NumericSuffix::Mm,
10996            },
10997        };
10998        let constraint = Constraint::Distance(Distance {
10999            segments: vec![point0_id.into(), point1_id.into()],
11000            distance: Number {
11001                value: 2.0,
11002                units: NumericSuffix::Mm,
11003            },
11004            label_position: Some(label_position.clone()),
11005            source: Default::default(),
11006        });
11007        let (src_delta, scene_delta) = frontend
11008            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11009            .await
11010            .unwrap();
11011        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11012
11013        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11014        let sketch = expect_sketch(sketch_object);
11015        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11016        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11017            panic!("Expected constraint object");
11018        };
11019        let Constraint::Distance(distance) = constraint else {
11020            panic!("Expected distance constraint");
11021        };
11022        assert_eq!(distance.label_position, Some(label_position));
11023
11024        mock_ctx.close().await;
11025    }
11026
11027    #[tokio::test(flavor = "multi_thread")]
11028    async fn test_edit_distance_constraint_label_position() {
11029        let initial_source = "\
11030sketch(on = XY) {
11031  point(at = [var 1, var 2])
11032  point(at = [var 3, var 2])
11033}
11034";
11035
11036        let program = Program::parse(initial_source).unwrap().0.unwrap();
11037
11038        let mut frontend = FrontendState::new();
11039
11040        let mock_ctx = ExecutorContext::new_mock(None).await;
11041        let version = Version(0);
11042
11043        frontend.program = program.clone();
11044        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11045        frontend.update_state_after_exec(outcome, true);
11046        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11047        let sketch_id = sketch_object.id;
11048        let sketch = expect_sketch(sketch_object);
11049        let point0_id = *sketch.segments.first().unwrap();
11050        let point1_id = *sketch.segments.get(1).unwrap();
11051
11052        let constraint = Constraint::Distance(Distance {
11053            segments: vec![point0_id.into(), point1_id.into()],
11054            distance: Number {
11055                value: 2.0,
11056                units: NumericSuffix::Mm,
11057            },
11058            label_position: None,
11059            source: Default::default(),
11060        });
11061        let (_, scene_delta) = frontend
11062            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11063            .await
11064            .unwrap();
11065        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11066        let sketch = expect_sketch(sketch_object);
11067        let constraint_id = sketch.constraints[0];
11068        let label_position = Point2d {
11069            x: Number {
11070                value: 10.0,
11071                units: NumericSuffix::Mm,
11072            },
11073            y: Number {
11074                value: 11.0,
11075                units: NumericSuffix::Mm,
11076            },
11077        };
11078
11079        let (src_delta, scene_delta) = frontend
11080            .edit_distance_constraint_label_position(
11081                &mock_ctx,
11082                version,
11083                sketch_id,
11084                constraint_id,
11085                label_position.clone(),
11086                vec![],
11087            )
11088            .await
11089            .unwrap();
11090        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11091
11092        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11093        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11094            panic!("Expected constraint object");
11095        };
11096        let Constraint::Distance(distance) = constraint else {
11097            panic!("Expected distance constraint");
11098        };
11099        assert_eq!(distance.label_position, Some(label_position));
11100
11101        mock_ctx.close().await;
11102    }
11103
11104    #[tokio::test(flavor = "multi_thread")]
11105    async fn test_edit_distance_constraint_type_and_value() {
11106        let initial_source = "\
11107sketch(on = XY) {
11108  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11109  distance([line1.start, line1.end]) == 5mm
11110}
11111";
11112
11113        let program = Program::parse(initial_source).unwrap().0.unwrap();
11114        let mut frontend = FrontendState::new();
11115        let mock_ctx = ExecutorContext::new_mock(None).await;
11116        let version = Version(0);
11117
11118        frontend.program = program.clone();
11119        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11120        frontend.update_state_after_exec(outcome, true);
11121        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11122        let sketch_id = sketch_object.id;
11123        let sketch = expect_sketch(sketch_object);
11124        let constraint_id = sketch.constraints[0];
11125        let point0_id = sketch.segments[0];
11126        let point1_id = sketch.segments[1];
11127        let label_position = Point2d {
11128            x: Number {
11129                value: 2.0,
11130                units: NumericSuffix::Mm,
11131            },
11132            y: Number {
11133                value: 5.0,
11134                units: NumericSuffix::Mm,
11135            },
11136        };
11137
11138        let (source_delta, scene_delta) = frontend
11139            .edit_distance_constraint_with_options(
11140                &mock_ctx,
11141                version,
11142                sketch_id,
11143                constraint_id,
11144                Constraint::HorizontalDistance(Distance {
11145                    segments: vec![point0_id.into(), point1_id.into()],
11146                    distance: Number {
11147                        value: 4.0,
11148                        units: NumericSuffix::Mm,
11149                    },
11150                    label_position: Some(label_position.clone()),
11151                    source: Default::default(),
11152                }),
11153                EditConstraintOptions {
11154                    commit_solved_initial_guesses: false,
11155                },
11156            )
11157            .await
11158            .unwrap();
11159        assert_eq!(
11160            source_delta.text,
11161            "\
11162sketch(on = XY) {
11163  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11164  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11165}
11166"
11167        );
11168
11169        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11170        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11171            panic!("Expected constraint object");
11172        };
11173        let Constraint::HorizontalDistance(distance) = constraint else {
11174            panic!("Expected horizontal distance constraint");
11175        };
11176        assert_eq!(distance.distance.value, 4.0);
11177        assert_eq!(distance.label_position, Some(label_position));
11178
11179        mock_ctx.close().await;
11180    }
11181
11182    #[tokio::test(flavor = "multi_thread")]
11183    async fn test_edit_angle_constraint_label_position() {
11184        let initial_source = "\
11185sketch(on = XY) {
11186  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11187  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11188  angle([line1, line2]) == 60deg
11189}
11190";
11191
11192        let program = Program::parse(initial_source).unwrap().0.unwrap();
11193        let mut frontend = FrontendState::new();
11194        let mock_ctx = ExecutorContext::new_mock(None).await;
11195        let version = Version(0);
11196
11197        frontend.program = program.clone();
11198        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11199        frontend.update_state_after_exec(outcome, true);
11200        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11201        let sketch_id = sketch_object.id;
11202        let sketch = expect_sketch(sketch_object);
11203        let constraint_id = sketch.constraints[0];
11204        let label_position = Point2d {
11205            x: Number {
11206                value: 10.0,
11207                units: NumericSuffix::Mm,
11208            },
11209            y: Number {
11210                value: 11.0,
11211                units: NumericSuffix::Mm,
11212            },
11213        };
11214
11215        let (src_delta, scene_delta) = frontend
11216            .edit_distance_constraint_label_position(
11217                &mock_ctx,
11218                version,
11219                sketch_id,
11220                constraint_id,
11221                label_position.clone(),
11222                vec![],
11223            )
11224            .await
11225            .unwrap();
11226        assert_eq!(
11227            src_delta.text.as_str(),
11228            "\
11229sketch(on = XY) {
11230  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11231  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11232  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11233}
11234"
11235        );
11236
11237        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11238        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11239            panic!("Expected constraint object");
11240        };
11241        let Constraint::Angle(angle) = constraint else {
11242            panic!("Expected angle constraint");
11243        };
11244        assert_eq!(angle.label_position, Some(label_position));
11245
11246        mock_ctx.close().await;
11247    }
11248
11249    #[tokio::test(flavor = "multi_thread")]
11250    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11251        let initial_source = "\
11252sketch(on = XY) {
11253  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11254  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11255  60deg == angleDimension(lines = [line1, line2], sector = 1)
11256}
11257";
11258
11259        let program = Program::parse(initial_source).unwrap().0.unwrap();
11260        let mut frontend = FrontendState::new();
11261        let mock_ctx = ExecutorContext::new_mock(None).await;
11262        let version = Version(0);
11263
11264        frontend.program = program.clone();
11265        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11266        frontend.update_state_after_exec(outcome, true);
11267        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11268        let sketch_id = sketch_object.id;
11269        let sketch = expect_sketch(sketch_object);
11270        let constraint_id = sketch.constraints[0];
11271        let label_position = Point2d {
11272            x: Number {
11273                value: 10.0,
11274                units: NumericSuffix::Mm,
11275            },
11276            y: Number {
11277                value: 11.0,
11278                units: NumericSuffix::Mm,
11279            },
11280        };
11281
11282        let (src_delta, scene_delta) = frontend
11283            .edit_distance_constraint_label_position(
11284                &mock_ctx,
11285                version,
11286                sketch_id,
11287                constraint_id,
11288                label_position.clone(),
11289                vec![],
11290            )
11291            .await
11292            .unwrap();
11293        assert_eq!(
11294            src_delta.text.as_str(),
11295            "\
11296sketch(on = XY) {
11297  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11298  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11299  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11300}
11301"
11302        );
11303
11304        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11305        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11306            panic!("Expected constraint object");
11307        };
11308        let Constraint::Angle(angle) = constraint else {
11309            panic!("Expected angle constraint");
11310        };
11311        assert_eq!(angle.label_position, Some(label_position));
11312
11313        mock_ctx.close().await;
11314    }
11315
11316    #[tokio::test(flavor = "multi_thread")]
11317    async fn test_edit_angle_constraint() {
11318        let initial_source = "\
11319sketch(on = XY) {
11320  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11321  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11322  angle([line1, line2]) == 60deg
11323}
11324";
11325
11326        let program = Program::parse(initial_source).unwrap().0.unwrap();
11327        let mut frontend = FrontendState::new();
11328        let mock_ctx = ExecutorContext::new_mock(None).await;
11329        let version = Version(0);
11330
11331        frontend.program = program.clone();
11332        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11333        frontend.update_state_after_exec(outcome, true);
11334        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11335        let sketch_id = sketch_object.id;
11336        let sketch = expect_sketch(sketch_object);
11337        let constraint_id = sketch.constraints[0];
11338        let line1_id = *sketch.segments.get(2).unwrap();
11339        let line2_id = *sketch.segments.get(5).unwrap();
11340        let label_position = Point2d {
11341            x: Number {
11342                value: 10.0,
11343                units: NumericSuffix::Mm,
11344            },
11345            y: Number {
11346                value: 11.0,
11347                units: NumericSuffix::Mm,
11348            },
11349        };
11350
11351        let (src_delta, scene_delta) = frontend
11352            .edit_angle_constraint_with_options(
11353                &mock_ctx,
11354                version,
11355                sketch_id,
11356                constraint_id,
11357                Angle {
11358                    lines: vec![line2_id, line1_id],
11359                    angle: Number {
11360                        value: 60.0,
11361                        units: NumericSuffix::Deg,
11362                    },
11363                    sector: Some(3),
11364                    inverse: Some(false),
11365                    label_position: Some(label_position.clone()),
11366                    source: Default::default(),
11367                },
11368                EditConstraintOptions {
11369                    commit_solved_initial_guesses: false,
11370                },
11371            )
11372            .await
11373            .unwrap();
11374        assert_eq!(
11375            src_delta.text.as_str(),
11376            "\
11377sketch(on = XY) {
11378  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11379  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11380  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11381}
11382"
11383        );
11384
11385        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11386        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11387            panic!("Expected constraint object");
11388        };
11389        let Constraint::Angle(angle) = constraint else {
11390            panic!("Expected angle constraint");
11391        };
11392        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11393        assert_eq!(angle.sector, Some(3));
11394        assert_eq!(angle.inverse, Some(false));
11395        assert_eq!(angle.label_position, Some(label_position));
11396
11397        mock_ctx.close().await;
11398    }
11399
11400    #[tokio::test(flavor = "multi_thread")]
11401    async fn test_edit_angle_constraint_with_call_on_right() {
11402        let initial_source = "\
11403sketch(on = XY) {
11404  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11405  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11406  60deg == angle([line1, line2])
11407}
11408";
11409
11410        let program = Program::parse(initial_source).unwrap().0.unwrap();
11411        let mut frontend = FrontendState::new();
11412        let mock_ctx = ExecutorContext::new_mock(None).await;
11413        let version = Version(0);
11414
11415        frontend.program = program.clone();
11416        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11417        frontend.update_state_after_exec(outcome, true);
11418        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11419        let sketch_id = sketch_object.id;
11420        let sketch = expect_sketch(sketch_object);
11421        let constraint_id = sketch.constraints[0];
11422        let line1_id = *sketch.segments.get(2).unwrap();
11423        let line2_id = *sketch.segments.get(5).unwrap();
11424
11425        let (src_delta, _) = frontend
11426            .edit_angle_constraint_with_options(
11427                &mock_ctx,
11428                version,
11429                sketch_id,
11430                constraint_id,
11431                Angle {
11432                    lines: vec![line2_id, line1_id],
11433                    angle: Number {
11434                        value: 60.0,
11435                        units: NumericSuffix::Deg,
11436                    },
11437                    sector: Some(3),
11438                    inverse: Some(false),
11439                    label_position: None,
11440                    source: Default::default(),
11441                },
11442                EditConstraintOptions {
11443                    commit_solved_initial_guesses: false,
11444                },
11445            )
11446            .await
11447            .unwrap();
11448        assert_eq!(
11449            src_delta.text.as_str(),
11450            "\
11451sketch(on = XY) {
11452  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11453  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11454  60deg == angleDimension(lines = [line2, line1], sector = 3)
11455}
11456"
11457        );
11458
11459        mock_ctx.close().await;
11460    }
11461
11462    #[tokio::test(flavor = "multi_thread")]
11463    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11464        let initial_source = "\
11465@settings(kclVersion = 2.0)
11466
11467sketch001 = sketch(on = XZ) {
11468  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11469  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11470  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11471  vertical([line1.start, ORIGIN])
11472}
11473";
11474
11475        let program = Program::parse(initial_source).unwrap().0.unwrap();
11476        let mut frontend = FrontendState::new();
11477        let mock_ctx = ExecutorContext::new_mock(None).await;
11478        let version = Version(0);
11479
11480        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11481        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11482        let sketch_id = sketch_object.id;
11483        let sketch = expect_sketch(sketch_object);
11484        let constraint_id = sketch
11485            .constraints
11486            .iter()
11487            .copied()
11488            .find(|constraint_id| {
11489                matches!(
11490                    frontend.scene_graph.objects[constraint_id.0].kind,
11491                    ObjectKind::Constraint {
11492                        constraint: Constraint::Distance(_)
11493                    }
11494                )
11495            })
11496            .unwrap();
11497        let line1_id = sketch
11498            .segments
11499            .iter()
11500            .copied()
11501            .find(|segment_id| {
11502                matches!(
11503                    frontend.scene_graph.objects[segment_id.0].kind,
11504                    ObjectKind::Segment {
11505                        segment: Segment::Line(_)
11506                    }
11507                )
11508            })
11509            .unwrap();
11510        let label_position = Point2d {
11511            x: Number {
11512                value: 7.0,
11513                units: NumericSuffix::Mm,
11514            },
11515            y: Number {
11516                value: 9.0,
11517                units: NumericSuffix::Mm,
11518            },
11519        };
11520
11521        let (source_delta, scene_delta) = frontend
11522            .edit_segments_with_options(
11523                &mock_ctx,
11524                version,
11525                sketch_id,
11526                vec![ExistingSegmentCtor {
11527                    id: line1_id,
11528                    ctor: SegmentCtor::Line(LineCtor {
11529                        start: point_expr_mm(2.0, 15.55),
11530                        end: point_expr_mm(-4.03, 11.51),
11531                        construction: None,
11532                    }),
11533                }],
11534                EditSegmentsOptions {
11535                    anchor_segment_ids: Some(vec![]),
11536                    drag_anchors: vec![SegmentDragAnchor {
11537                        segment_id: line1_id,
11538                        target: label_position.clone(),
11539                    }],
11540                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11541                        constraint_id,
11542                        label_position: label_position.clone(),
11543                    }],
11544                    commit_solved_initial_guesses: true,
11545                },
11546            )
11547            .await
11548            .unwrap();
11549
11550        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11551        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11552        let ObjectKind::Constraint {
11553            constraint: Constraint::Distance(distance),
11554        } = &constraint_object.kind
11555        else {
11556            panic!("Expected distance constraint object");
11557        };
11558        assert_eq!(distance.label_position, Some(label_position));
11559
11560        let snapped_label_position = Point2d {
11561            x: Number {
11562                value: 8.0,
11563                units: NumericSuffix::Mm,
11564            },
11565            y: Number {
11566                value: 10.0,
11567                units: NumericSuffix::Mm,
11568            },
11569        };
11570        let (source_delta, scene_delta) = frontend
11571            .edit_segments_with_options(
11572                &mock_ctx,
11573                version,
11574                sketch_id,
11575                vec![],
11576                EditSegmentsOptions {
11577                    anchor_segment_ids: Some(vec![line1_id]),
11578                    drag_anchors: vec![],
11579                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11580                        constraint_id,
11581                        label_position: snapped_label_position.clone(),
11582                    }],
11583                    commit_solved_initial_guesses: true,
11584                },
11585            )
11586            .await
11587            .unwrap();
11588
11589        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11590        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11591        let ObjectKind::Constraint {
11592            constraint: Constraint::Distance(distance),
11593        } = &constraint_object.kind
11594        else {
11595            panic!("Expected distance constraint object");
11596        };
11597        assert_eq!(distance.label_position, Some(snapped_label_position));
11598
11599        mock_ctx.close().await;
11600    }
11601
11602    #[tokio::test(flavor = "multi_thread")]
11603    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11604        let initial_source = "\
11605sketch(on = XY) {
11606  point1 = point(at = [var 0mm, var 0mm])
11607  point2 = point(at = [var 10mm, var 0mm])
11608  distance([point1, point2]) == 5mm
11609}
11610";
11611
11612        let program = Program::parse(initial_source).unwrap().0.unwrap();
11613        let mut frontend = FrontendState::new();
11614        let mock_ctx = ExecutorContext::new_mock(None).await;
11615        let version = Version(0);
11616
11617        frontend.program = program.clone();
11618        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11619        frontend.update_state_after_exec(outcome, true);
11620        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11621        let sketch_id = sketch_object.id;
11622        let sketch = expect_sketch(sketch_object);
11623        let point0_id = sketch.segments[0];
11624        let point1_id = sketch.segments[1];
11625        let constraint_id = sketch.constraints[0];
11626
11627        let edited_segments = vec![ExistingSegmentCtor {
11628            id: point0_id,
11629            ctor: SegmentCtor::Point(PointCtor {
11630                position: Point2d {
11631                    x: Expr::Var(Number {
11632                        value: 2.0,
11633                        units: NumericSuffix::Mm,
11634                    }),
11635                    y: Expr::Var(Number {
11636                        value: 1.0,
11637                        units: NumericSuffix::Mm,
11638                    }),
11639                },
11640            }),
11641        }];
11642        let (_, scene_delta) = frontend
11643            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11644            .await
11645            .unwrap();
11646        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11647        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11648
11649        let label_position = Point2d {
11650            x: Number {
11651                value: 3.0,
11652                units: NumericSuffix::Mm,
11653            },
11654            y: Number {
11655                value: 4.0,
11656                units: NumericSuffix::Mm,
11657            },
11658        };
11659        let (_, scene_delta) = frontend
11660            .edit_distance_constraint_label_position(
11661                &mock_ctx,
11662                version,
11663                sketch_id,
11664                constraint_id,
11665                label_position,
11666                vec![point0_id],
11667            )
11668            .await
11669            .unwrap();
11670
11671        assert_point_position_close(
11672            point_position(&scene_delta.new_graph, point0_id),
11673            point0_after_segment_edit,
11674        );
11675        assert_point_position_close(
11676            point_position(&scene_delta.new_graph, point1_id),
11677            point1_after_segment_edit,
11678        );
11679
11680        mock_ctx.close().await;
11681    }
11682
11683    #[tokio::test(flavor = "multi_thread")]
11684    async fn test_distance_point_line() {
11685        let initial_source = "\
11686sketch(on = XY) {
11687  point(at = [var 0, var 5])
11688  line(start = [var 0, var 0], end = [var 10, var 0])
11689}
11690";
11691
11692        let program = Program::parse(initial_source).unwrap().0.unwrap();
11693
11694        let mut frontend = FrontendState::new();
11695
11696        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11697        let mock_ctx = ExecutorContext::new_mock(None).await;
11698        let version = Version(0);
11699
11700        frontend.hack_set_program(&ctx, program).await.unwrap();
11701        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11702        let sketch_id = sketch_object.id;
11703        let sketch = expect_sketch(sketch_object);
11704        let point_id = *sketch.segments.first().unwrap();
11705        let line_id = *sketch
11706            .segments
11707            .iter()
11708            .find(|segment_id| {
11709                matches!(
11710                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11711                    Some(ObjectKind::Segment {
11712                        segment: Segment::Line(_)
11713                    })
11714                )
11715            })
11716            .unwrap();
11717
11718        let label_position = Point2d {
11719            x: Number {
11720                value: 10.0,
11721                units: NumericSuffix::Mm,
11722            },
11723            y: Number {
11724                value: 11.0,
11725                units: NumericSuffix::Mm,
11726            },
11727        };
11728        let constraint = Constraint::Distance(Distance {
11729            segments: vec![point_id.into(), line_id.into()],
11730            distance: Number {
11731                value: 5.0,
11732                units: NumericSuffix::Mm,
11733            },
11734            label_position: Some(label_position.clone()),
11735            source: Default::default(),
11736        });
11737        let (src_delta, scene_delta) = frontend
11738            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11739            .await
11740            .unwrap();
11741        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11742        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11743        let sketch = expect_sketch(sketch_object);
11744        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11745        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11746            panic!("Expected constraint object");
11747        };
11748        let Constraint::Distance(distance) = constraint else {
11749            panic!("Expected distance constraint");
11750        };
11751        assert_eq!(distance.label_position, Some(label_position));
11752
11753        ctx.close().await;
11754        mock_ctx.close().await;
11755    }
11756
11757    #[tokio::test(flavor = "multi_thread")]
11758    async fn test_distance_point_arc() {
11759        let initial_source = "\
11760sketch(on = XY) {
11761  point(at = [var 0, var 8])
11762  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11763}
11764";
11765
11766        let program = Program::parse(initial_source).unwrap().0.unwrap();
11767
11768        let mut frontend = FrontendState::new();
11769
11770        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11771        let mock_ctx = ExecutorContext::new_mock(None).await;
11772        let version = Version(0);
11773
11774        frontend.hack_set_program(&ctx, program).await.unwrap();
11775        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11776        let sketch_id = sketch_object.id;
11777        let sketch = expect_sketch(sketch_object);
11778        let point_id = *sketch.segments.first().unwrap();
11779        let arc_id = *sketch
11780            .segments
11781            .iter()
11782            .find(|segment_id| {
11783                matches!(
11784                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11785                    Some(ObjectKind::Segment {
11786                        segment: Segment::Arc(_)
11787                    })
11788                )
11789            })
11790            .unwrap();
11791
11792        let constraint = Constraint::Distance(Distance {
11793            segments: vec![point_id.into(), arc_id.into()],
11794            distance: Number {
11795                value: 3.0,
11796                units: NumericSuffix::Mm,
11797            },
11798            label_position: None,
11799            source: Default::default(),
11800        });
11801        let (src_delta, _scene_delta) = frontend
11802            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11803            .await
11804            .unwrap();
11805        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11806
11807        ctx.close().await;
11808        mock_ctx.close().await;
11809    }
11810
11811    #[tokio::test(flavor = "multi_thread")]
11812    async fn test_distance_arc_origin() {
11813        let initial_source = "\
11814sketch001 = sketch(on = XY) {
11815  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11816}
11817";
11818
11819        let program = Program::parse(initial_source).unwrap().0.unwrap();
11820
11821        let mut frontend = FrontendState::new();
11822
11823        let mock_ctx = ExecutorContext::new_mock(None).await;
11824        let version = Version(0);
11825
11826        frontend.program = program.clone();
11827        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11828        frontend.update_state_after_exec(outcome, true);
11829        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11830        let sketch_id = sketch_object.id;
11831        let sketch = expect_sketch(sketch_object);
11832        let arc_id = *sketch
11833            .segments
11834            .iter()
11835            .find(|segment_id| {
11836                matches!(
11837                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11838                    Some(ObjectKind::Segment {
11839                        segment: Segment::Arc(_)
11840                    })
11841                )
11842            })
11843            .unwrap();
11844
11845        let constraint = Constraint::Distance(Distance {
11846            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11847            distance: Number {
11848                value: 3.0,
11849                units: NumericSuffix::Mm,
11850            },
11851            label_position: None,
11852            source: Default::default(),
11853        });
11854        let (src_delta, _scene_delta) = frontend
11855            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11856            .await
11857            .unwrap();
11858        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11859
11860        mock_ctx.close().await;
11861    }
11862
11863    #[tokio::test(flavor = "multi_thread")]
11864    async fn test_distance_line_origin() {
11865        let initial_source = "\
11866sketch(on = XY) {
11867  line(start = [var 5, var 0], end = [var 5, var 10])
11868}
11869";
11870
11871        let program = Program::parse(initial_source).unwrap().0.unwrap();
11872
11873        let mut frontend = FrontendState::new();
11874
11875        let mock_ctx = ExecutorContext::new_mock(None).await;
11876        let version = Version(0);
11877
11878        frontend.program = program.clone();
11879        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11880        frontend.update_state_after_exec(outcome, true);
11881        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11882        let sketch_id = sketch_object.id;
11883        let sketch = expect_sketch(sketch_object);
11884        let line_id = *sketch
11885            .segments
11886            .iter()
11887            .find(|segment_id| {
11888                matches!(
11889                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11890                    Some(ObjectKind::Segment {
11891                        segment: Segment::Line(_)
11892                    })
11893                )
11894            })
11895            .unwrap();
11896
11897        let constraint = Constraint::Distance(Distance {
11898            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11899            distance: Number {
11900                value: 5.0,
11901                units: NumericSuffix::Mm,
11902            },
11903            label_position: None,
11904            source: Default::default(),
11905        });
11906        let (src_delta, _scene_delta) = frontend
11907            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11908            .await
11909            .unwrap();
11910        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11911
11912        mock_ctx.close().await;
11913    }
11914
11915    #[tokio::test(flavor = "multi_thread")]
11916    async fn test_distance_line_circle() {
11917        let initial_source = "\
11918sketch(on = XY) {
11919  line(start = [var -10, var 8], end = [var 10, var 8])
11920  circle(start = [var 5, var 0], center = [var 0, var 0])
11921}
11922";
11923
11924        let program = Program::parse(initial_source).unwrap().0.unwrap();
11925
11926        let mut frontend = FrontendState::new();
11927
11928        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11929        let mock_ctx = ExecutorContext::new_mock(None).await;
11930        let version = Version(0);
11931
11932        frontend.hack_set_program(&ctx, program).await.unwrap();
11933        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11934        let sketch_id = sketch_object.id;
11935        let sketch = expect_sketch(sketch_object);
11936        let line_id = *sketch
11937            .segments
11938            .iter()
11939            .find(|segment_id| {
11940                matches!(
11941                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11942                    Some(ObjectKind::Segment {
11943                        segment: Segment::Line(_)
11944                    })
11945                )
11946            })
11947            .unwrap();
11948        let circle_id = *sketch
11949            .segments
11950            .iter()
11951            .find(|segment_id| {
11952                matches!(
11953                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11954                    Some(ObjectKind::Segment {
11955                        segment: Segment::Circle(_)
11956                    })
11957                )
11958            })
11959            .unwrap();
11960
11961        let constraint = Constraint::Distance(Distance {
11962            segments: vec![line_id.into(), circle_id.into()],
11963            distance: Number {
11964                value: 3.0,
11965                units: NumericSuffix::Mm,
11966            },
11967            label_position: None,
11968            source: Default::default(),
11969        });
11970        let (src_delta, _scene_delta) = frontend
11971            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11972            .await
11973            .unwrap();
11974        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11975
11976        ctx.close().await;
11977        mock_ctx.close().await;
11978    }
11979
11980    #[tokio::test(flavor = "multi_thread")]
11981    async fn test_distance_circle_arc() {
11982        let initial_source = "\
11983sketch(on = XY) {
11984  circle(start = [var 5, var 0], center = [var 0, var 0])
11985  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11986}
11987";
11988
11989        let program = Program::parse(initial_source).unwrap().0.unwrap();
11990
11991        let mut frontend = FrontendState::new();
11992
11993        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11994        let mock_ctx = ExecutorContext::new_mock(None).await;
11995        let version = Version(0);
11996
11997        frontend.hack_set_program(&ctx, program).await.unwrap();
11998        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11999        let sketch_id = sketch_object.id;
12000        let sketch = expect_sketch(sketch_object);
12001        let circle_id = *sketch
12002            .segments
12003            .iter()
12004            .find(|segment_id| {
12005                matches!(
12006                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12007                    Some(ObjectKind::Segment {
12008                        segment: Segment::Circle(_)
12009                    })
12010                )
12011            })
12012            .unwrap();
12013        let arc_id = *sketch
12014            .segments
12015            .iter()
12016            .find(|segment_id| {
12017                matches!(
12018                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12019                    Some(ObjectKind::Segment {
12020                        segment: Segment::Arc(_)
12021                    })
12022                )
12023            })
12024            .unwrap();
12025
12026        let constraint = Constraint::Distance(Distance {
12027            segments: vec![circle_id.into(), arc_id.into()],
12028            distance: Number {
12029                value: 3.0,
12030                units: NumericSuffix::Mm,
12031            },
12032            label_position: None,
12033            source: Default::default(),
12034        });
12035        let (src_delta, _scene_delta) = frontend
12036            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12037            .await
12038            .unwrap();
12039        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12040
12041        ctx.close().await;
12042        mock_ctx.close().await;
12043    }
12044
12045    #[tokio::test(flavor = "multi_thread")]
12046    async fn test_distance_parallel_lines() {
12047        let initial_source = "\
12048sketch(on = XY) {
12049  line(start = [var 0, var 0], end = [var 10, var 0])
12050  line(start = [var 0, var 5], end = [var 10, var 5])
12051}
12052";
12053
12054        let program = Program::parse(initial_source).unwrap().0.unwrap();
12055
12056        let mut frontend = FrontendState::new();
12057
12058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12059        let mock_ctx = ExecutorContext::new_mock(None).await;
12060        let version = Version(0);
12061
12062        frontend.hack_set_program(&ctx, program).await.unwrap();
12063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12064        let sketch_id = sketch_object.id;
12065        let sketch = expect_sketch(sketch_object);
12066        let line_ids = sketch
12067            .segments
12068            .iter()
12069            .copied()
12070            .filter(|segment_id| {
12071                matches!(
12072                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12073                    Some(ObjectKind::Segment {
12074                        segment: Segment::Line(_)
12075                    })
12076                )
12077            })
12078            .collect::<Vec<_>>();
12079
12080        let constraint = Constraint::Distance(Distance {
12081            segments: vec![line_ids[0].into(), line_ids[1].into()],
12082            distance: Number {
12083                value: 5.0,
12084                units: NumericSuffix::Mm,
12085            },
12086            label_position: None,
12087            source: Default::default(),
12088        });
12089        let (src_delta, _scene_delta) = frontend
12090            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12091            .await
12092            .unwrap();
12093        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12094
12095        ctx.close().await;
12096        mock_ctx.close().await;
12097    }
12098
12099    #[tokio::test(flavor = "multi_thread")]
12100    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12101        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12102        // one another because the gradient of the residual has no tangential component in this
12103        // configuration. The only path to reducing the residual is shrinking the lengths of the
12104        // lines which are causing the lines to collapse, producing a degenerate output.
12105        let initial_source = "\
12106sketch(on = XY) {
12107  line(start = [var 0, var 0], end = [var 10, var 0])
12108  line(start = [var 0, var 0], end = [var 10, var 10])
12109}
12110";
12111
12112        let program = Program::parse(initial_source).unwrap().0.unwrap();
12113
12114        let mut frontend = FrontendState::new();
12115
12116        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12117        let mock_ctx = ExecutorContext::new_mock(None).await;
12118        let version = Version(0);
12119
12120        frontend.hack_set_program(&ctx, program).await.unwrap();
12121        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12122        let sketch_id = sketch_object.id;
12123        let sketch = expect_sketch(sketch_object);
12124        let line_ids = sketch
12125            .segments
12126            .iter()
12127            .copied()
12128            .filter(|segment_id| {
12129                matches!(
12130                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12131                    Some(ObjectKind::Segment {
12132                        segment: Segment::Line(_)
12133                    })
12134                )
12135            })
12136            .collect::<Vec<_>>();
12137
12138        let constraint = Constraint::Distance(Distance {
12139            segments: vec![line_ids[0].into(), line_ids[1].into()],
12140            distance: Number {
12141                value: 5.0,
12142                units: NumericSuffix::Mm,
12143            },
12144            label_position: None,
12145            source: Default::default(),
12146        });
12147        let (src_delta, _scene_delta) = frontend
12148            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12149            .await
12150            .unwrap();
12151        insta::assert_snapshot!(
12152            "test_distance_non_parallel_lines_lowers_to_distance",
12153            src_delta.text.as_str()
12154        );
12155
12156        ctx.close().await;
12157        mock_ctx.close().await;
12158    }
12159
12160    #[tokio::test(flavor = "multi_thread")]
12161    async fn test_horizontal_distance_two_points() {
12162        let initial_source = "\
12163sketch(on = XY) {
12164  point(at = [var 1, var 2])
12165  point(at = [var 3, var 4])
12166}
12167";
12168
12169        let program = Program::parse(initial_source).unwrap().0.unwrap();
12170
12171        let mut frontend = FrontendState::new();
12172
12173        let mock_ctx = ExecutorContext::new_mock(None).await;
12174        let version = Version(0);
12175
12176        frontend.program = program.clone();
12177        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12178        frontend.update_state_after_exec(outcome, true);
12179        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180        let sketch_id = sketch_object.id;
12181        let sketch = expect_sketch(sketch_object);
12182        let point0_id = *sketch.segments.first().unwrap();
12183        let point1_id = *sketch.segments.get(1).unwrap();
12184        let label_position = Point2d {
12185            x: Number {
12186                value: 10.0,
12187                units: NumericSuffix::Mm,
12188            },
12189            y: Number {
12190                value: 11.0,
12191                units: NumericSuffix::Mm,
12192            },
12193        };
12194
12195        let constraint = Constraint::HorizontalDistance(Distance {
12196            segments: vec![point0_id.into(), point1_id.into()],
12197            distance: Number {
12198                value: 2.0,
12199                units: NumericSuffix::Mm,
12200            },
12201            label_position: Some(label_position.clone()),
12202            source: Default::default(),
12203        });
12204        let (src_delta, scene_delta) = frontend
12205            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12206            .await
12207            .unwrap();
12208        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12209        assert_eq!(
12210            scene_delta.new_graph.objects.len(),
12211            5,
12212            "{:#?}",
12213            scene_delta.new_graph.objects
12214        );
12215        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12216        let sketch = expect_sketch(sketch_object);
12217        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12218        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12219            panic!("Expected constraint object");
12220        };
12221        let Constraint::HorizontalDistance(distance) = constraint else {
12222            panic!("Expected horizontal distance constraint");
12223        };
12224        assert_eq!(distance.label_position, Some(label_position));
12225
12226        mock_ctx.close().await;
12227    }
12228
12229    #[tokio::test(flavor = "multi_thread")]
12230    async fn test_radius_single_arc_segment() {
12231        let initial_source = "\
12232sketch(on = XY) {
12233  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12234}
12235";
12236
12237        let program = Program::parse(initial_source).unwrap().0.unwrap();
12238
12239        let mut frontend = FrontendState::new();
12240
12241        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12242        let mock_ctx = ExecutorContext::new_mock(None).await;
12243        let version = Version(0);
12244
12245        frontend.hack_set_program(&ctx, program).await.unwrap();
12246        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12247        let sketch_id = sketch_object.id;
12248        let sketch = expect_sketch(sketch_object);
12249        // Find the arc segment (not the points)
12250        let arc_id = sketch
12251            .segments
12252            .iter()
12253            .find(|&seg_id| {
12254                let obj = frontend.scene_graph.objects.get(seg_id.0);
12255                matches!(
12256                    obj.map(|o| &o.kind),
12257                    Some(ObjectKind::Segment {
12258                        segment: Segment::Arc(_)
12259                    })
12260                )
12261            })
12262            .unwrap();
12263
12264        let constraint = Constraint::Radius(Radius {
12265            arc: *arc_id,
12266            radius: Number {
12267                value: 5.0,
12268                units: NumericSuffix::Mm,
12269            },
12270            label_position: None,
12271            source: Default::default(),
12272        });
12273        let (src_delta, scene_delta) = frontend
12274            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12275            .await
12276            .unwrap();
12277        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12278        assert_eq!(
12279            scene_delta.new_graph.objects.len(),
12280            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12281            "{:#?}",
12282            scene_delta.new_graph.objects
12283        );
12284
12285        ctx.close().await;
12286        mock_ctx.close().await;
12287    }
12288
12289    #[tokio::test(flavor = "multi_thread")]
12290    async fn test_radius_single_arc_segment_with_label_position() {
12291        let initial_source = "\
12292sketch(on = XY) {
12293  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12294}
12295";
12296
12297        let program = Program::parse(initial_source).unwrap().0.unwrap();
12298        let mut frontend = FrontendState::new();
12299        let mock_ctx = ExecutorContext::new_mock(None).await;
12300        let version = Version(0);
12301
12302        frontend.program = program.clone();
12303        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12304        frontend.update_state_after_exec(outcome, true);
12305        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12306        let sketch_id = sketch_object.id;
12307        let sketch = expect_sketch(sketch_object);
12308        let arc_id = sketch
12309            .segments
12310            .iter()
12311            .find(|&seg_id| {
12312                let obj = frontend.scene_graph.objects.get(seg_id.0);
12313                matches!(
12314                    obj.map(|o| &o.kind),
12315                    Some(ObjectKind::Segment {
12316                        segment: Segment::Arc(_)
12317                    })
12318                )
12319            })
12320            .unwrap();
12321
12322        let label_position = Point2d {
12323            x: Number {
12324                value: 10.0,
12325                units: NumericSuffix::Mm,
12326            },
12327            y: Number {
12328                value: 11.0,
12329                units: NumericSuffix::Mm,
12330            },
12331        };
12332        let constraint = Constraint::Radius(Radius {
12333            arc: *arc_id,
12334            radius: Number {
12335                value: 5.0,
12336                units: NumericSuffix::Mm,
12337            },
12338            label_position: Some(label_position.clone()),
12339            source: Default::default(),
12340        });
12341        let (src_delta, scene_delta) = frontend
12342            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12343            .await
12344            .unwrap();
12345        insta::assert_snapshot!(
12346            "test_radius_single_arc_segment_with_label_position",
12347            src_delta.text.as_str()
12348        );
12349
12350        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12351        let sketch = expect_sketch(sketch_object);
12352        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12353        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12354            panic!("Expected constraint object");
12355        };
12356        let Constraint::Radius(radius) = constraint else {
12357            panic!("Expected radius constraint");
12358        };
12359        assert_eq!(radius.label_position, Some(label_position));
12360
12361        mock_ctx.close().await;
12362    }
12363
12364    #[tokio::test(flavor = "multi_thread")]
12365    async fn test_edit_radius_constraint_label_position() {
12366        let initial_source = "\
12367sketch(on = XY) {
12368  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12369  radius(arc1) == 5mm
12370}
12371";
12372
12373        let program = Program::parse(initial_source).unwrap().0.unwrap();
12374        let mut frontend = FrontendState::new();
12375        let mock_ctx = ExecutorContext::new_mock(None).await;
12376        let version = Version(0);
12377
12378        frontend.program = program.clone();
12379        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12380        frontend.update_state_after_exec(outcome, true);
12381        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12382        let sketch_id = sketch_object.id;
12383        let sketch = expect_sketch(sketch_object);
12384        let constraint_id = sketch.constraints[0];
12385        let label_position = Point2d {
12386            x: Number {
12387                value: 10.0,
12388                units: NumericSuffix::Mm,
12389            },
12390            y: Number {
12391                value: 11.0,
12392                units: NumericSuffix::Mm,
12393            },
12394        };
12395
12396        let (src_delta, scene_delta) = frontend
12397            .edit_distance_constraint_label_position(
12398                &mock_ctx,
12399                version,
12400                sketch_id,
12401                constraint_id,
12402                label_position.clone(),
12403                vec![],
12404            )
12405            .await
12406            .unwrap();
12407        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12408
12409        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12410        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12411            panic!("Expected constraint object");
12412        };
12413        let Constraint::Radius(radius) = constraint else {
12414            panic!("Expected radius constraint");
12415        };
12416        assert_eq!(radius.label_position, Some(label_position));
12417
12418        mock_ctx.close().await;
12419    }
12420
12421    #[tokio::test(flavor = "multi_thread")]
12422    async fn test_vertical_distance_two_points() {
12423        let initial_source = "\
12424sketch(on = XY) {
12425  point(at = [var 1, var 2])
12426  point(at = [var 3, var 4])
12427}
12428";
12429
12430        let program = Program::parse(initial_source).unwrap().0.unwrap();
12431
12432        let mut frontend = FrontendState::new();
12433
12434        let mock_ctx = ExecutorContext::new_mock(None).await;
12435        let version = Version(0);
12436
12437        frontend.program = program.clone();
12438        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12439        frontend.update_state_after_exec(outcome, true);
12440        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12441        let sketch_id = sketch_object.id;
12442        let sketch = expect_sketch(sketch_object);
12443        let point0_id = *sketch.segments.first().unwrap();
12444        let point1_id = *sketch.segments.get(1).unwrap();
12445        let label_position = Point2d {
12446            x: Number {
12447                value: 10.0,
12448                units: NumericSuffix::Mm,
12449            },
12450            y: Number {
12451                value: 11.0,
12452                units: NumericSuffix::Mm,
12453            },
12454        };
12455
12456        let constraint = Constraint::VerticalDistance(Distance {
12457            segments: vec![point0_id.into(), point1_id.into()],
12458            distance: Number {
12459                value: 2.0,
12460                units: NumericSuffix::Mm,
12461            },
12462            label_position: Some(label_position.clone()),
12463            source: Default::default(),
12464        });
12465        let (src_delta, scene_delta) = frontend
12466            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12467            .await
12468            .unwrap();
12469        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12470        assert_eq!(
12471            scene_delta.new_graph.objects.len(),
12472            5,
12473            "{:#?}",
12474            scene_delta.new_graph.objects
12475        );
12476        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12477        let sketch = expect_sketch(sketch_object);
12478        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12479        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12480            panic!("Expected constraint object");
12481        };
12482        let Constraint::VerticalDistance(distance) = constraint else {
12483            panic!("Expected vertical distance constraint");
12484        };
12485        assert_eq!(distance.label_position, Some(label_position));
12486
12487        mock_ctx.close().await;
12488    }
12489
12490    #[tokio::test(flavor = "multi_thread")]
12491    async fn test_add_fixed_standalone_point() {
12492        let initial_source = "\
12493sketch(on = XY) {
12494  point(at = [var 1, var 2])
12495}
12496";
12497
12498        let program = Program::parse(initial_source).unwrap().0.unwrap();
12499
12500        let mut frontend = FrontendState::new();
12501
12502        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12503        let mock_ctx = ExecutorContext::new_mock(None).await;
12504        let version = Version(0);
12505
12506        frontend.hack_set_program(&ctx, program).await.unwrap();
12507        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12508        let sketch_id = sketch_object.id;
12509        let sketch = expect_sketch(sketch_object);
12510        let point_id = *sketch.segments.first().unwrap();
12511
12512        let (src_delta, scene_delta) = frontend
12513            .add_constraint(
12514                &mock_ctx,
12515                version,
12516                sketch_id,
12517                Constraint::Fixed(Fixed {
12518                    points: vec![FixedPoint {
12519                        point: point_id,
12520                        position: Point2d {
12521                            x: Number {
12522                                value: 2.0,
12523                                units: NumericSuffix::Mm,
12524                            },
12525                            y: Number {
12526                                value: 3.0,
12527                                units: NumericSuffix::Mm,
12528                            },
12529                        },
12530                    }],
12531                }),
12532            )
12533            .await
12534            .unwrap();
12535        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12536        assert_eq!(
12537            scene_delta.new_graph.objects.len(),
12538            4,
12539            "{:#?}",
12540            scene_delta.new_graph.objects
12541        );
12542
12543        ctx.close().await;
12544        mock_ctx.close().await;
12545    }
12546
12547    #[tokio::test(flavor = "multi_thread")]
12548    async fn test_add_fixed_multiple_points() {
12549        let initial_source = "\
12550sketch(on = XY) {
12551  point(at = [var 1, var 2])
12552  point(at = [var 3, var 4])
12553}
12554";
12555
12556        let program = Program::parse(initial_source).unwrap().0.unwrap();
12557
12558        let mut frontend = FrontendState::new();
12559
12560        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12561        let mock_ctx = ExecutorContext::new_mock(None).await;
12562        let version = Version(0);
12563
12564        frontend.hack_set_program(&ctx, program).await.unwrap();
12565        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12566        let sketch_id = sketch_object.id;
12567        let sketch = expect_sketch(sketch_object);
12568        let point0_id = *sketch.segments.first().unwrap();
12569        let point1_id = *sketch.segments.get(1).unwrap();
12570
12571        let (src_delta, scene_delta) = frontend
12572            .add_constraint(
12573                &mock_ctx,
12574                version,
12575                sketch_id,
12576                Constraint::Fixed(Fixed {
12577                    points: vec![
12578                        FixedPoint {
12579                            point: point0_id,
12580                            position: Point2d {
12581                                x: Number {
12582                                    value: 2.0,
12583                                    units: NumericSuffix::Mm,
12584                                },
12585                                y: Number {
12586                                    value: 3.0,
12587                                    units: NumericSuffix::Mm,
12588                                },
12589                            },
12590                        },
12591                        FixedPoint {
12592                            point: point1_id,
12593                            position: Point2d {
12594                                x: Number {
12595                                    value: 4.0,
12596                                    units: NumericSuffix::Mm,
12597                                },
12598                                y: Number {
12599                                    value: 5.0,
12600                                    units: NumericSuffix::Mm,
12601                                },
12602                            },
12603                        },
12604                    ],
12605                }),
12606            )
12607            .await
12608            .unwrap();
12609        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12610        assert_eq!(
12611            scene_delta.new_graph.objects.len(),
12612            6,
12613            "{:#?}",
12614            scene_delta.new_graph.objects
12615        );
12616
12617        ctx.close().await;
12618        mock_ctx.close().await;
12619    }
12620
12621    #[tokio::test(flavor = "multi_thread")]
12622    async fn test_add_fixed_owned_point() {
12623        let initial_source = "\
12624sketch(on = XY) {
12625  line(start = [var 1, var 2], end = [var 3, var 4])
12626}
12627";
12628
12629        let program = Program::parse(initial_source).unwrap().0.unwrap();
12630
12631        let mut frontend = FrontendState::new();
12632
12633        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12634        let mock_ctx = ExecutorContext::new_mock(None).await;
12635        let version = Version(0);
12636
12637        frontend.hack_set_program(&ctx, program).await.unwrap();
12638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12639        let sketch_id = sketch_object.id;
12640        let sketch = expect_sketch(sketch_object);
12641        let line_start_id = *sketch.segments.first().unwrap();
12642
12643        let (src_delta, scene_delta) = frontend
12644            .add_constraint(
12645                &mock_ctx,
12646                version,
12647                sketch_id,
12648                Constraint::Fixed(Fixed {
12649                    points: vec![FixedPoint {
12650                        point: line_start_id,
12651                        position: Point2d {
12652                            x: Number {
12653                                value: 2.0,
12654                                units: NumericSuffix::Mm,
12655                            },
12656                            y: Number {
12657                                value: 3.0,
12658                                units: NumericSuffix::Mm,
12659                            },
12660                        },
12661                    }],
12662                }),
12663            )
12664            .await
12665            .unwrap();
12666        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12667        assert_eq!(
12668            scene_delta.new_graph.objects.len(),
12669            6,
12670            "{:#?}",
12671            scene_delta.new_graph.objects
12672        );
12673
12674        ctx.close().await;
12675        mock_ctx.close().await;
12676    }
12677
12678    #[tokio::test(flavor = "multi_thread")]
12679    async fn test_radius_error_cases() {
12680        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12681        let mock_ctx = ExecutorContext::new_mock(None).await;
12682        let version = Version(0);
12683
12684        // Test: Single point should error
12685        let initial_source_point = "\
12686sketch(on = XY) {
12687  point(at = [var 1, var 2])
12688}
12689";
12690        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12691        let mut frontend_point = FrontendState::new();
12692        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12693        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12694        let sketch_id_point = sketch_object_point.id;
12695        let sketch_point = expect_sketch(sketch_object_point);
12696        let point_id = *sketch_point.segments.first().unwrap();
12697
12698        let constraint_point = Constraint::Radius(Radius {
12699            arc: point_id,
12700            radius: Number {
12701                value: 5.0,
12702                units: NumericSuffix::Mm,
12703            },
12704            label_position: None,
12705            source: Default::default(),
12706        });
12707        let result_point = frontend_point
12708            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12709            .await;
12710        assert!(result_point.is_err(), "Single point should error for radius");
12711
12712        // Test: Single line segment should error (only arc segments supported)
12713        let initial_source_line = "\
12714sketch(on = XY) {
12715  line(start = [var 1, var 2], end = [var 3, var 4])
12716}
12717";
12718        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12719        let mut frontend_line = FrontendState::new();
12720        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12721        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12722        let sketch_id_line = sketch_object_line.id;
12723        let sketch_line = expect_sketch(sketch_object_line);
12724        let line_id = *sketch_line.segments.first().unwrap();
12725
12726        let constraint_line = Constraint::Radius(Radius {
12727            arc: line_id,
12728            radius: Number {
12729                value: 5.0,
12730                units: NumericSuffix::Mm,
12731            },
12732            label_position: None,
12733            source: Default::default(),
12734        });
12735        let result_line = frontend_line
12736            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12737            .await;
12738        assert!(result_line.is_err(), "Single line segment should error for radius");
12739
12740        ctx.close().await;
12741        mock_ctx.close().await;
12742    }
12743
12744    #[tokio::test(flavor = "multi_thread")]
12745    async fn test_diameter_single_arc_segment() {
12746        let initial_source = "\
12747sketch(on = XY) {
12748  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12749}
12750";
12751
12752        let program = Program::parse(initial_source).unwrap().0.unwrap();
12753
12754        let mut frontend = FrontendState::new();
12755
12756        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12757        let mock_ctx = ExecutorContext::new_mock(None).await;
12758        let version = Version(0);
12759
12760        frontend.hack_set_program(&ctx, program).await.unwrap();
12761        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12762        let sketch_id = sketch_object.id;
12763        let sketch = expect_sketch(sketch_object);
12764        // Find the arc segment (not the points)
12765        let arc_id = sketch
12766            .segments
12767            .iter()
12768            .find(|&seg_id| {
12769                let obj = frontend.scene_graph.objects.get(seg_id.0);
12770                matches!(
12771                    obj.map(|o| &o.kind),
12772                    Some(ObjectKind::Segment {
12773                        segment: Segment::Arc(_)
12774                    })
12775                )
12776            })
12777            .unwrap();
12778
12779        let constraint = Constraint::Diameter(Diameter {
12780            arc: *arc_id,
12781            diameter: Number {
12782                value: 10.0,
12783                units: NumericSuffix::Mm,
12784            },
12785            label_position: None,
12786            source: Default::default(),
12787        });
12788        let (src_delta, scene_delta) = frontend
12789            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12790            .await
12791            .unwrap();
12792        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12793        assert_eq!(
12794            scene_delta.new_graph.objects.len(),
12795            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12796            "{:#?}",
12797            scene_delta.new_graph.objects
12798        );
12799
12800        ctx.close().await;
12801        mock_ctx.close().await;
12802    }
12803
12804    #[tokio::test(flavor = "multi_thread")]
12805    async fn test_diameter_single_arc_segment_with_label_position() {
12806        let initial_source = "\
12807sketch(on = XY) {
12808  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12809}
12810";
12811
12812        let program = Program::parse(initial_source).unwrap().0.unwrap();
12813        let mut frontend = FrontendState::new();
12814        let mock_ctx = ExecutorContext::new_mock(None).await;
12815        let version = Version(0);
12816
12817        frontend.program = program.clone();
12818        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12819        frontend.update_state_after_exec(outcome, true);
12820        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12821        let sketch_id = sketch_object.id;
12822        let sketch = expect_sketch(sketch_object);
12823        let arc_id = sketch
12824            .segments
12825            .iter()
12826            .find(|&seg_id| {
12827                let obj = frontend.scene_graph.objects.get(seg_id.0);
12828                matches!(
12829                    obj.map(|o| &o.kind),
12830                    Some(ObjectKind::Segment {
12831                        segment: Segment::Arc(_)
12832                    })
12833                )
12834            })
12835            .unwrap();
12836
12837        let label_position = Point2d {
12838            x: Number {
12839                value: 10.0,
12840                units: NumericSuffix::Mm,
12841            },
12842            y: Number {
12843                value: 11.0,
12844                units: NumericSuffix::Mm,
12845            },
12846        };
12847        let constraint = Constraint::Diameter(Diameter {
12848            arc: *arc_id,
12849            diameter: Number {
12850                value: 10.0,
12851                units: NumericSuffix::Mm,
12852            },
12853            label_position: Some(label_position.clone()),
12854            source: Default::default(),
12855        });
12856        let (src_delta, scene_delta) = frontend
12857            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12858            .await
12859            .unwrap();
12860        insta::assert_snapshot!(
12861            "test_diameter_single_arc_segment_with_label_position",
12862            src_delta.text.as_str()
12863        );
12864
12865        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12866        let sketch = expect_sketch(sketch_object);
12867        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12868        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12869            panic!("Expected constraint object");
12870        };
12871        let Constraint::Diameter(diameter) = constraint else {
12872            panic!("Expected diameter constraint");
12873        };
12874        assert_eq!(diameter.label_position, Some(label_position));
12875
12876        mock_ctx.close().await;
12877    }
12878
12879    #[tokio::test(flavor = "multi_thread")]
12880    async fn test_edit_diameter_constraint_label_position() {
12881        let initial_source = "\
12882sketch(on = XY) {
12883  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12884  diameter(arc1) == 10mm
12885}
12886";
12887
12888        let program = Program::parse(initial_source).unwrap().0.unwrap();
12889        let mut frontend = FrontendState::new();
12890        let mock_ctx = ExecutorContext::new_mock(None).await;
12891        let version = Version(0);
12892
12893        frontend.program = program.clone();
12894        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12895        frontend.update_state_after_exec(outcome, true);
12896        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12897        let sketch_id = sketch_object.id;
12898        let sketch = expect_sketch(sketch_object);
12899        let constraint_id = sketch.constraints[0];
12900        let label_position = Point2d {
12901            x: Number {
12902                value: 10.0,
12903                units: NumericSuffix::Mm,
12904            },
12905            y: Number {
12906                value: 11.0,
12907                units: NumericSuffix::Mm,
12908            },
12909        };
12910
12911        let (src_delta, scene_delta) = frontend
12912            .edit_distance_constraint_label_position(
12913                &mock_ctx,
12914                version,
12915                sketch_id,
12916                constraint_id,
12917                label_position.clone(),
12918                vec![],
12919            )
12920            .await
12921            .unwrap();
12922        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12923
12924        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12925        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12926            panic!("Expected constraint object");
12927        };
12928        let Constraint::Diameter(diameter) = constraint else {
12929            panic!("Expected diameter constraint");
12930        };
12931        assert_eq!(diameter.label_position, Some(label_position));
12932
12933        mock_ctx.close().await;
12934    }
12935
12936    #[tokio::test(flavor = "multi_thread")]
12937    async fn test_diameter_error_cases() {
12938        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12939        let mock_ctx = ExecutorContext::new_mock(None).await;
12940        let version = Version(0);
12941
12942        // Test: Single point should error
12943        let initial_source_point = "\
12944sketch(on = XY) {
12945  point(at = [var 1, var 2])
12946}
12947";
12948        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12949        let mut frontend_point = FrontendState::new();
12950        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12951        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12952        let sketch_id_point = sketch_object_point.id;
12953        let sketch_point = expect_sketch(sketch_object_point);
12954        let point_id = *sketch_point.segments.first().unwrap();
12955
12956        let constraint_point = Constraint::Diameter(Diameter {
12957            arc: point_id,
12958            diameter: Number {
12959                value: 10.0,
12960                units: NumericSuffix::Mm,
12961            },
12962            label_position: None,
12963            source: Default::default(),
12964        });
12965        let result_point = frontend_point
12966            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12967            .await;
12968        assert!(result_point.is_err(), "Single point should error for diameter");
12969
12970        // Test: Single line segment should error (only arc segments supported)
12971        let initial_source_line = "\
12972sketch(on = XY) {
12973  line(start = [var 1, var 2], end = [var 3, var 4])
12974}
12975";
12976        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12977        let mut frontend_line = FrontendState::new();
12978        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12979        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12980        let sketch_id_line = sketch_object_line.id;
12981        let sketch_line = expect_sketch(sketch_object_line);
12982        let line_id = *sketch_line.segments.first().unwrap();
12983
12984        let constraint_line = Constraint::Diameter(Diameter {
12985            arc: line_id,
12986            diameter: Number {
12987                value: 10.0,
12988                units: NumericSuffix::Mm,
12989            },
12990            label_position: None,
12991            source: Default::default(),
12992        });
12993        let result_line = frontend_line
12994            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12995            .await;
12996        assert!(result_line.is_err(), "Single line segment should error for diameter");
12997
12998        ctx.close().await;
12999        mock_ctx.close().await;
13000    }
13001
13002    #[tokio::test(flavor = "multi_thread")]
13003    async fn test_line_horizontal() {
13004        let initial_source = "\
13005sketch(on = XY) {
13006  line(start = [var 1, var 2], end = [var 3, var 4])
13007}
13008";
13009
13010        let program = Program::parse(initial_source).unwrap().0.unwrap();
13011
13012        let mut frontend = FrontendState::new();
13013
13014        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13015        let mock_ctx = ExecutorContext::new_mock(None).await;
13016        let version = Version(0);
13017
13018        frontend.hack_set_program(&ctx, program).await.unwrap();
13019        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13020        let sketch_id = sketch_object.id;
13021        let sketch = expect_sketch(sketch_object);
13022        let line1_id = *sketch.segments.get(2).unwrap();
13023
13024        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13025        let (src_delta, scene_delta) = frontend
13026            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13027            .await
13028            .unwrap();
13029        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13030        assert_eq!(
13031            scene_delta.new_graph.objects.len(),
13032            6,
13033            "{:#?}",
13034            scene_delta.new_graph.objects
13035        );
13036
13037        ctx.close().await;
13038        mock_ctx.close().await;
13039    }
13040
13041    #[tokio::test(flavor = "multi_thread")]
13042    async fn test_control_point_spline_edge_horizontal() {
13043        let initial_source = "\
13044@settings(experimentalFeatures = allow)
13045splineSketch = sketch(on = XY) {
13046  controlPointSpline1 = controlPointSpline(points = [
13047    [var 0mm, var 0mm],
13048    [var 10mm, var 20mm],
13049    [var 20mm, var 0mm],
13050  ])
13051}
13052";
13053
13054        let program = Program::parse(initial_source).unwrap().0.unwrap();
13055
13056        let mut frontend = FrontendState::new();
13057
13058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13059        let mock_ctx = ExecutorContext::new_mock(None).await;
13060        let version = Version(0);
13061
13062        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13064        let sketch_id = sketch_object.id;
13065        let sketch = expect_sketch(sketch_object);
13066        let spline_id = sketch
13067            .segments
13068            .iter()
13069            .copied()
13070            .find(|seg_id| {
13071                matches!(
13072                    &frontend.scene_graph.objects[seg_id.0].kind,
13073                    ObjectKind::Segment {
13074                        segment: Segment::ControlPointSpline(_)
13075                    }
13076                )
13077            })
13078            .expect("Expected a control point spline segment in sketch");
13079        let edge_id = frontend
13080            .scene_graph
13081            .objects
13082            .iter()
13083            .find_map(|obj| match &obj.kind {
13084                ObjectKind::Segment {
13085                    segment: Segment::Line(line),
13086                } if line.owner == Some(spline_id) => Some(obj.id),
13087                _ => None,
13088            })
13089            .expect("Expected an owned control-polygon edge");
13090
13091        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13092        let (src_delta, _) = frontend
13093            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13094            .await
13095            .unwrap();
13096        assert!(
13097            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13098            "Expected horizontal constraint on spline edge, got: {}",
13099            src_delta.text
13100        );
13101
13102        ctx.close().await;
13103        mock_ctx.close().await;
13104    }
13105
13106    #[tokio::test(flavor = "multi_thread")]
13107    async fn test_control_point_spline_edge_angle() {
13108        let initial_source = "\
13109@settings(experimentalFeatures = allow)
13110splineSketch = sketch(on = XY) {
13111  controlPointSpline1 = controlPointSpline(points = [
13112    [var 0mm, var 0mm],
13113    [var 10mm, var 20mm],
13114    [var 20mm, var 0mm],
13115  ])
13116
13117  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13118}
13119";
13120
13121        let program = Program::parse(initial_source).unwrap().0.unwrap();
13122
13123        let mut frontend = FrontendState::new();
13124
13125        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13126        let mock_ctx = ExecutorContext::new_mock(None).await;
13127        let version = Version(0);
13128
13129        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13130        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13131        let sketch_id = sketch_object.id;
13132        let sketch = expect_sketch(sketch_object);
13133        let spline_id = sketch
13134            .segments
13135            .iter()
13136            .copied()
13137            .find(|seg_id| {
13138                matches!(
13139                    &frontend.scene_graph.objects[seg_id.0].kind,
13140                    ObjectKind::Segment {
13141                        segment: Segment::ControlPointSpline(_)
13142                    }
13143                )
13144            })
13145            .expect("Expected a control point spline segment in sketch");
13146        let edge_id = frontend
13147            .scene_graph
13148            .objects
13149            .iter()
13150            .find_map(|obj| match &obj.kind {
13151                ObjectKind::Segment {
13152                    segment: Segment::Line(line),
13153                } if line.owner == Some(spline_id) => Some(obj.id),
13154                _ => None,
13155            })
13156            .expect("Expected an owned control-polygon edge");
13157        let line1_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.is_none() && obj.label == "line1" => Some(obj.id),
13165                _ => None,
13166            })
13167            .or_else(|| {
13168                sketch.segments.iter().copied().find(|seg_id| {
13169                    matches!(
13170                        &frontend.scene_graph.objects[seg_id.0].kind,
13171                        ObjectKind::Segment {
13172                            segment: Segment::Line(line),
13173                        } if line.owner.is_none()
13174                    )
13175                })
13176            })
13177            .expect("Expected a standalone line segment in sketch");
13178
13179        let constraint = Constraint::Angle(Angle {
13180            lines: vec![line1_id, edge_id],
13181            angle: Number {
13182                value: 30.0,
13183                units: NumericSuffix::Deg,
13184            },
13185            sector: None,
13186            inverse: None,
13187            label_position: None,
13188            source: Default::default(),
13189        });
13190        let (src_delta, _) = frontend
13191            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13192            .await
13193            .unwrap();
13194        assert!(
13195            src_delta
13196                .text
13197                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13198            "Expected angle constraint on spline edge, got: {}",
13199            src_delta.text
13200        );
13201
13202        ctx.close().await;
13203        mock_ctx.close().await;
13204    }
13205
13206    #[tokio::test(flavor = "multi_thread")]
13207    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13208        let initial_source = "\
13209@settings(experimentalFeatures = allow)
13210splineSketch = sketch(on = XY) {
13211  spline1 = controlPointSpline(points = [
13212    [var 0mm, var 0mm],
13213    [var 10mm, var 20mm],
13214    [var 20mm, var 0mm],
13215  ])
13216  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13217  coincident([spline1.controls[1], spline1.edges[0]])
13218  coincident([spline1.controls[0], line1])
13219}
13220";
13221
13222        let program = Program::parse(initial_source).unwrap().0.unwrap();
13223
13224        let mut frontend = FrontendState::new();
13225
13226        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13227        let mock_ctx = ExecutorContext::new_mock(None).await;
13228
13229        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13230
13231        let ui_scene_graph = frontend.scene_graph_for_ui();
13232        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13233        let sketch = expect_sketch(sketch_object);
13234
13235        assert_eq!(
13236            sketch.constraints.len(),
13237            1,
13238            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13239        );
13240
13241        let visible_constraints = ui_scene_graph
13242            .objects
13243            .iter()
13244            .filter_map(|object| match &object.kind {
13245                ObjectKind::Constraint {
13246                    constraint: Constraint::Coincident(coincident),
13247                } => Some(coincident.clone()),
13248                _ => None,
13249            })
13250            .collect::<Vec<_>>();
13251
13252        assert_eq!(
13253            visible_constraints.len(),
13254            1,
13255            "Expected only one coincident constraint object in the UI scene graph"
13256        );
13257        assert_eq!(
13258            visible_constraints[0].get_segments().len(),
13259            2,
13260            "Expected the remaining visible coincident constraint to reference two segments"
13261        );
13262
13263        ctx.close().await;
13264        mock_ctx.close().await;
13265    }
13266
13267    #[tokio::test(flavor = "multi_thread")]
13268    async fn test_edit_control_point_spline_can_append_control_point() {
13269        let initial_source = "\
13270@settings(experimentalFeatures = allow)
13271splineSketch = sketch(on = XY) {
13272  controlPointSpline(points = [
13273    [var 0mm, var 0mm],
13274    [var 10mm, var 20mm],
13275    [var 20mm, var 0mm],
13276  ])
13277}
13278";
13279
13280        let program = Program::parse(initial_source).unwrap().0.unwrap();
13281
13282        let mut frontend = FrontendState::new();
13283
13284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13285        let mock_ctx = ExecutorContext::new_mock(None).await;
13286        let version = Version(0);
13287
13288        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13289        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13290        let sketch_id = sketch_object.id;
13291        let sketch = expect_sketch(sketch_object);
13292        let spline_id = sketch
13293            .segments
13294            .iter()
13295            .copied()
13296            .find(|seg_id| {
13297                matches!(
13298                    &frontend.scene_graph.objects[seg_id.0].kind,
13299                    ObjectKind::Segment {
13300                        segment: Segment::ControlPointSpline(_)
13301                    }
13302                )
13303            })
13304            .expect("Expected a control point spline segment in sketch");
13305
13306        let ctor = ControlPointSplineCtor {
13307            points: vec![
13308                Point2d {
13309                    x: Expr::Var(Number {
13310                        value: 0.0,
13311                        units: NumericSuffix::Mm,
13312                    }),
13313                    y: Expr::Var(Number {
13314                        value: 0.0,
13315                        units: NumericSuffix::Mm,
13316                    }),
13317                },
13318                Point2d {
13319                    x: Expr::Var(Number {
13320                        value: 10.0,
13321                        units: NumericSuffix::Mm,
13322                    }),
13323                    y: Expr::Var(Number {
13324                        value: 20.0,
13325                        units: NumericSuffix::Mm,
13326                    }),
13327                },
13328                Point2d {
13329                    x: Expr::Var(Number {
13330                        value: 20.0,
13331                        units: NumericSuffix::Mm,
13332                    }),
13333                    y: Expr::Var(Number {
13334                        value: 0.0,
13335                        units: NumericSuffix::Mm,
13336                    }),
13337                },
13338                Point2d {
13339                    x: Expr::Var(Number {
13340                        value: 30.0,
13341                        units: NumericSuffix::Mm,
13342                    }),
13343                    y: Expr::Var(Number {
13344                        value: 10.0,
13345                        units: NumericSuffix::Mm,
13346                    }),
13347                },
13348            ],
13349            construction: None,
13350        };
13351
13352        let segments = vec![ExistingSegmentCtor {
13353            id: spline_id,
13354            ctor: SegmentCtor::ControlPointSpline(ctor),
13355        }];
13356        let (src_delta, scene_delta) = frontend
13357            .edit_segments(&mock_ctx, version, sketch_id, segments)
13358            .await
13359            .unwrap();
13360
13361        assert!(
13362            src_delta.text.contains("[var 30mm, var 10mm]"),
13363            "Expected appended spline control point in source, got: {}",
13364            src_delta.text
13365        );
13366
13367        assert!(
13368            scene_delta.invalidates_ids,
13369            "Expected appending a spline control point to invalidate ids"
13370        );
13371        let updated_spline = scene_delta
13372            .new_graph
13373            .objects
13374            .iter()
13375            .find_map(|obj| match &obj.kind {
13376                ObjectKind::Segment {
13377                    segment: Segment::ControlPointSpline(updated_spline),
13378                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13379                _ => None,
13380            })
13381            .expect("Expected edited scene graph to contain a four-point control point spline");
13382        assert_eq!(
13383            updated_spline.controls.len(),
13384            4,
13385            "Expected edited spline to expose four control points"
13386        );
13387
13388        ctx.close().await;
13389        mock_ctx.close().await;
13390    }
13391
13392    #[tokio::test(flavor = "multi_thread")]
13393    async fn test_line_vertical() {
13394        let initial_source = "\
13395sketch(on = XY) {
13396  line(start = [var 1, var 2], end = [var 3, var 4])
13397}
13398";
13399
13400        let program = Program::parse(initial_source).unwrap().0.unwrap();
13401
13402        let mut frontend = FrontendState::new();
13403
13404        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13405        let mock_ctx = ExecutorContext::new_mock(None).await;
13406        let version = Version(0);
13407
13408        frontend.hack_set_program(&ctx, program).await.unwrap();
13409        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13410        let sketch_id = sketch_object.id;
13411        let sketch = expect_sketch(sketch_object);
13412        let line1_id = *sketch.segments.get(2).unwrap();
13413
13414        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13415        let (src_delta, scene_delta) = frontend
13416            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13417            .await
13418            .unwrap();
13419        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13420        assert_eq!(
13421            scene_delta.new_graph.objects.len(),
13422            6,
13423            "{:#?}",
13424            scene_delta.new_graph.objects
13425        );
13426
13427        ctx.close().await;
13428        mock_ctx.close().await;
13429    }
13430
13431    #[tokio::test(flavor = "multi_thread")]
13432    async fn test_points_vertical() {
13433        let initial_source = "\
13434sketch001 = sketch(on = XY) {
13435  p0 = point(at = [var -2.23mm, var 3.1mm])
13436  pf = point(at = [4, 4])
13437}
13438";
13439
13440        let program = Program::parse(initial_source).unwrap().0.unwrap();
13441
13442        let mut frontend = FrontendState::new();
13443
13444        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13445        let mock_ctx = ExecutorContext::new_mock(None).await;
13446        let version = Version(0);
13447
13448        frontend.hack_set_program(&ctx, program).await.unwrap();
13449        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13450        let sketch_id = sketch_object.id;
13451        let sketch = expect_sketch(sketch_object);
13452        let point_ids = vec![
13453            sketch.segments.first().unwrap().to_owned(),
13454            sketch.segments.get(1).unwrap().to_owned(),
13455        ];
13456
13457        let constraint = Constraint::Vertical(Vertical::Points {
13458            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13459        });
13460        let (src_delta, scene_delta) = frontend
13461            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13462            .await
13463            .unwrap();
13464        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13465        assert_eq!(
13466            scene_delta.new_graph.objects.len(),
13467            5,
13468            "{:#?}",
13469            scene_delta.new_graph.objects
13470        );
13471
13472        ctx.close().await;
13473        mock_ctx.close().await;
13474    }
13475
13476    #[tokio::test(flavor = "multi_thread")]
13477    async fn test_points_horizontal() {
13478        let initial_source = "\
13479sketch001 = sketch(on = XY) {
13480  p0 = point(at = [var -2.23mm, var 3.1mm])
13481  pf = point(at = [4, 4])
13482}
13483";
13484
13485        let program = Program::parse(initial_source).unwrap().0.unwrap();
13486
13487        let mut frontend = FrontendState::new();
13488
13489        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13490        let mock_ctx = ExecutorContext::new_mock(None).await;
13491        let version = Version(0);
13492
13493        frontend.hack_set_program(&ctx, program).await.unwrap();
13494        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13495        let sketch_id = sketch_object.id;
13496        let sketch = expect_sketch(sketch_object);
13497        let point_ids = vec![
13498            sketch.segments.first().unwrap().to_owned(),
13499            sketch.segments.get(1).unwrap().to_owned(),
13500        ];
13501
13502        let constraint = Constraint::Horizontal(Horizontal::Points {
13503            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13504        });
13505        let (src_delta, scene_delta) = frontend
13506            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13507            .await
13508            .unwrap();
13509        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13510        assert_eq!(
13511            scene_delta.new_graph.objects.len(),
13512            5,
13513            "{:#?}",
13514            scene_delta.new_graph.objects
13515        );
13516
13517        ctx.close().await;
13518        mock_ctx.close().await;
13519    }
13520
13521    #[tokio::test(flavor = "multi_thread")]
13522    async fn test_point_horizontal_with_origin() {
13523        let initial_source = "\
13524sketch001 = sketch(on = XY) {
13525  p0 = point(at = [var -2.23mm, var 3.1mm])
13526}
13527";
13528
13529        let program = Program::parse(initial_source).unwrap().0.unwrap();
13530
13531        let mut frontend = FrontendState::new();
13532
13533        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13534        let mock_ctx = ExecutorContext::new_mock(None).await;
13535        let version = Version(0);
13536
13537        frontend.hack_set_program(&ctx, program).await.unwrap();
13538        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13539        let sketch_id = sketch_object.id;
13540        let sketch = expect_sketch(sketch_object);
13541        let point_id = *sketch.segments.first().unwrap();
13542
13543        let constraint = Constraint::Horizontal(Horizontal::Points {
13544            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13545        });
13546        let (src_delta, scene_delta) = frontend
13547            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13548            .await
13549            .unwrap();
13550        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13551        assert_eq!(
13552            scene_delta.new_graph.objects.len(),
13553            4,
13554            "{:#?}",
13555            scene_delta.new_graph.objects
13556        );
13557
13558        ctx.close().await;
13559        mock_ctx.close().await;
13560    }
13561
13562    #[tokio::test(flavor = "multi_thread")]
13563    async fn test_lines_equal_length() {
13564        let initial_source = "\
13565sketch(on = XY) {
13566  line(start = [var 1, var 2], end = [var 3, var 4])
13567  line(start = [var 5, var 6], end = [var 7, var 8])
13568}
13569";
13570
13571        let program = Program::parse(initial_source).unwrap().0.unwrap();
13572
13573        let mut frontend = FrontendState::new();
13574
13575        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13576        let mock_ctx = ExecutorContext::new_mock(None).await;
13577        let version = Version(0);
13578
13579        frontend.hack_set_program(&ctx, program).await.unwrap();
13580        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13581        let sketch_id = sketch_object.id;
13582        let sketch = expect_sketch(sketch_object);
13583        let line1_id = *sketch.segments.get(2).unwrap();
13584        let line2_id = *sketch.segments.get(5).unwrap();
13585
13586        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13587            lines: vec![line1_id, line2_id],
13588        });
13589        let (src_delta, scene_delta) = frontend
13590            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13591            .await
13592            .unwrap();
13593        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13594        assert_eq!(
13595            scene_delta.new_graph.objects.len(),
13596            9,
13597            "{:#?}",
13598            scene_delta.new_graph.objects
13599        );
13600
13601        ctx.close().await;
13602        mock_ctx.close().await;
13603    }
13604
13605    #[tokio::test(flavor = "multi_thread")]
13606    async fn test_add_constraint_multi_line_equal_length() {
13607        let initial_source = "\
13608sketch(on = XY) {
13609  line(start = [var 1, var 2], end = [var 3, var 4])
13610  line(start = [var 5, var 6], end = [var 7, var 8])
13611  line(start = [var 9, var 10], end = [var 11, var 12])
13612}
13613";
13614
13615        let program = Program::parse(initial_source).unwrap().0.unwrap();
13616
13617        let mut frontend = FrontendState::new();
13618        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13619        let mock_ctx = ExecutorContext::new_mock(None).await;
13620        let version = Version(0);
13621
13622        frontend.hack_set_program(&ctx, program).await.unwrap();
13623        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13624        let sketch_id = sketch_object.id;
13625        let sketch = expect_sketch(sketch_object);
13626        let line1_id = *sketch.segments.get(2).unwrap();
13627        let line2_id = *sketch.segments.get(5).unwrap();
13628        let line3_id = *sketch.segments.get(8).unwrap();
13629
13630        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13631            lines: vec![line1_id, line2_id, line3_id],
13632        });
13633        let (src_delta, scene_delta) = frontend
13634            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13635            .await
13636            .unwrap();
13637        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13638        let constraints = scene_delta
13639            .new_graph
13640            .objects
13641            .iter()
13642            .filter_map(|obj| {
13643                let ObjectKind::Constraint { constraint } = &obj.kind else {
13644                    return None;
13645                };
13646                Some(constraint)
13647            })
13648            .collect::<Vec<_>>();
13649
13650        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13651        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13652            panic!("expected equal length constraint, got {:?}", constraints[0]);
13653        };
13654        assert_eq!(lines_equal_length.lines.len(), 3);
13655
13656        ctx.close().await;
13657        mock_ctx.close().await;
13658    }
13659
13660    #[tokio::test(flavor = "multi_thread")]
13661    async fn test_lines_parallel() {
13662        let initial_source = "\
13663sketch(on = XY) {
13664  line(start = [var 1, var 2], end = [var 3, var 4])
13665  line(start = [var 5, var 6], end = [var 7, var 8])
13666}
13667";
13668
13669        let program = Program::parse(initial_source).unwrap().0.unwrap();
13670
13671        let mut frontend = FrontendState::new();
13672
13673        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13674        let mock_ctx = ExecutorContext::new_mock(None).await;
13675        let version = Version(0);
13676
13677        frontend.hack_set_program(&ctx, program).await.unwrap();
13678        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13679        let sketch_id = sketch_object.id;
13680        let sketch = expect_sketch(sketch_object);
13681        let line1_id = *sketch.segments.get(2).unwrap();
13682        let line2_id = *sketch.segments.get(5).unwrap();
13683
13684        let constraint = Constraint::Parallel(Parallel {
13685            lines: vec![line1_id, line2_id],
13686        });
13687        let (src_delta, scene_delta) = frontend
13688            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13689            .await
13690            .unwrap();
13691        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13692        assert_eq!(
13693            scene_delta.new_graph.objects.len(),
13694            9,
13695            "{:#?}",
13696            scene_delta.new_graph.objects
13697        );
13698
13699        ctx.close().await;
13700        mock_ctx.close().await;
13701    }
13702
13703    #[tokio::test(flavor = "multi_thread")]
13704    async fn test_lines_parallel_multiline() {
13705        let initial_source = "\
13706sketch(on = XY) {
13707  line(start = [var 1, var 2], end = [var 3, var 4])
13708  line(start = [var 5, var 6], end = [var 7, var 8])
13709  line(start = [var 9, var 10], end = [var 11, var 12])
13710}
13711";
13712
13713        let program = Program::parse(initial_source).unwrap().0.unwrap();
13714
13715        let mut frontend = FrontendState::new();
13716
13717        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13718        let mock_ctx = ExecutorContext::new_mock(None).await;
13719        let version = Version(0);
13720
13721        frontend.hack_set_program(&ctx, program).await.unwrap();
13722        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13723        let sketch_id = sketch_object.id;
13724        let sketch = expect_sketch(sketch_object);
13725        let line1_id = *sketch.segments.get(2).unwrap();
13726        let line2_id = *sketch.segments.get(5).unwrap();
13727        let line3_id = *sketch.segments.get(8).unwrap();
13728
13729        let constraint = Constraint::Parallel(Parallel {
13730            lines: vec![line1_id, line2_id, line3_id],
13731        });
13732        let (src_delta, scene_delta) = frontend
13733            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13734            .await
13735            .unwrap();
13736        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13737
13738        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13739        let sketch = expect_sketch(sketch_object);
13740        assert_eq!(sketch.constraints.len(), 1);
13741
13742        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13743        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13744            panic!("Expected constraint object");
13745        };
13746        let Constraint::Parallel(parallel) = constraint else {
13747            panic!("Expected parallel constraint");
13748        };
13749        assert_eq!(parallel.lines.len(), 3);
13750
13751        ctx.close().await;
13752        mock_ctx.close().await;
13753    }
13754
13755    #[tokio::test(flavor = "multi_thread")]
13756    async fn test_lines_perpendicular() {
13757        let initial_source = "\
13758sketch(on = XY) {
13759  line(start = [var 1, var 2], end = [var 3, var 4])
13760  line(start = [var 5, var 6], end = [var 7, var 8])
13761}
13762";
13763
13764        let program = Program::parse(initial_source).unwrap().0.unwrap();
13765
13766        let mut frontend = FrontendState::new();
13767
13768        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13769        let mock_ctx = ExecutorContext::new_mock(None).await;
13770        let version = Version(0);
13771
13772        frontend.hack_set_program(&ctx, program).await.unwrap();
13773        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13774        let sketch_id = sketch_object.id;
13775        let sketch = expect_sketch(sketch_object);
13776        let line1_id = *sketch.segments.get(2).unwrap();
13777        let line2_id = *sketch.segments.get(5).unwrap();
13778
13779        let constraint = Constraint::Perpendicular(Perpendicular {
13780            lines: vec![line1_id, line2_id],
13781        });
13782        let (src_delta, scene_delta) = frontend
13783            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13784            .await
13785            .unwrap();
13786        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13787        assert_eq!(
13788            scene_delta.new_graph.objects.len(),
13789            9,
13790            "{:#?}",
13791            scene_delta.new_graph.objects
13792        );
13793
13794        ctx.close().await;
13795        mock_ctx.close().await;
13796    }
13797
13798    #[tokio::test(flavor = "multi_thread")]
13799    async fn test_lines_angle() {
13800        let initial_source = "\
13801sketch(on = XY) {
13802  line(start = [var 1, var 2], end = [var 3, var 4])
13803  line(start = [var 5, var 6], end = [var 7, var 8])
13804}
13805";
13806
13807        let program = Program::parse(initial_source).unwrap().0.unwrap();
13808
13809        let mut frontend = FrontendState::new();
13810
13811        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13812        let mock_ctx = ExecutorContext::new_mock(None).await;
13813        let version = Version(0);
13814
13815        frontend.hack_set_program(&ctx, program).await.unwrap();
13816        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13817        let sketch_id = sketch_object.id;
13818        let sketch = expect_sketch(sketch_object);
13819        let line1_id = *sketch.segments.get(2).unwrap();
13820        let line2_id = *sketch.segments.get(5).unwrap();
13821
13822        let constraint = Constraint::Angle(Angle {
13823            lines: vec![line1_id, line2_id],
13824            angle: Number {
13825                value: 30.0,
13826                units: NumericSuffix::Deg,
13827            },
13828            sector: None,
13829            inverse: None,
13830            label_position: None,
13831            source: Default::default(),
13832        });
13833        let (src_delta, scene_delta) = frontend
13834            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13835            .await
13836            .unwrap();
13837        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13838        assert_eq!(
13839            scene_delta.new_graph.objects.len(),
13840            9,
13841            "{:#?}",
13842            scene_delta.new_graph.objects
13843        );
13844
13845        ctx.close().await;
13846        mock_ctx.close().await;
13847    }
13848
13849    #[tokio::test(flavor = "multi_thread")]
13850    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13851        let initial_source = "\
13852sketch(on = XY) {
13853  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13854  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13855}
13856";
13857
13858        let program = Program::parse(initial_source).unwrap().0.unwrap();
13859
13860        let mut frontend = FrontendState::new();
13861
13862        let mock_ctx = ExecutorContext::new_mock(None).await;
13863        let version = Version(0);
13864
13865        frontend.program = program.clone();
13866        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13867        frontend.update_state_after_exec(outcome, true);
13868        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13869        let sketch_id = sketch_object.id;
13870        let sketch = expect_sketch(sketch_object);
13871        let line1_id = *sketch.segments.get(2).unwrap();
13872        let line2_id = *sketch.segments.get(5).unwrap();
13873
13874        let constraint = Constraint::Angle(Angle {
13875            lines: vec![line1_id, line2_id],
13876            angle: Number {
13877                value: 270.0,
13878                units: NumericSuffix::Deg,
13879            },
13880            sector: Some(1),
13881            inverse: Some(true),
13882            label_position: Some(Point2d {
13883                x: Number {
13884                    value: -0.73,
13885                    units: NumericSuffix::Mm,
13886                },
13887                y: Number {
13888                    value: 0.75,
13889                    units: NumericSuffix::Mm,
13890                },
13891            }),
13892            source: Default::default(),
13893        });
13894        let (src_delta, _) = frontend
13895            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13896            .await
13897            .unwrap();
13898        assert_eq!(
13899            src_delta.text.as_str(),
13900            "\
13901sketch(on = XY) {
13902  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13903  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13904  angleDimension(
13905  lines = [line1, line2],
13906  sector = 1,
13907  inverse = true,
13908  labelPosition = [-0.73mm, 0.75mm],
13909) == 270deg
13910}
13911"
13912        );
13913
13914        mock_ctx.close().await;
13915    }
13916
13917    #[tokio::test(flavor = "multi_thread")]
13918    async fn test_segments_tangent() {
13919        let initial_source = "\
13920sketch(on = XY) {
13921  line(start = [var 1, var 2], end = [var 3, var 4])
13922  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13923}
13924";
13925
13926        let program = Program::parse(initial_source).unwrap().0.unwrap();
13927
13928        let mut frontend = FrontendState::new();
13929
13930        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13931        let mock_ctx = ExecutorContext::new_mock(None).await;
13932        let version = Version(0);
13933
13934        frontend.hack_set_program(&ctx, program).await.unwrap();
13935        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13936        let sketch_id = sketch_object.id;
13937        let sketch = expect_sketch(sketch_object);
13938        let line1_id = *sketch.segments.get(2).unwrap();
13939        let arc1_id = *sketch.segments.get(6).unwrap();
13940
13941        let constraint = Constraint::Tangent(Tangent {
13942            input: vec![line1_id, arc1_id],
13943        });
13944        let (src_delta, scene_delta) = frontend
13945            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13946            .await
13947            .unwrap();
13948        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13949        assert_eq!(
13950            scene_delta.new_graph.objects.len(),
13951            10,
13952            "{:#?}",
13953            scene_delta.new_graph.objects
13954        );
13955
13956        ctx.close().await;
13957        mock_ctx.close().await;
13958    }
13959
13960    #[tokio::test(flavor = "multi_thread")]
13961    async fn test_point_midpoint() {
13962        let initial_source = "\
13963sketch(on = XY) {
13964  point(at = [var 1, var 1])
13965  line(start = [var 0, var 0], end = [var 6, var 4])
13966}
13967";
13968
13969        let program = Program::parse(initial_source).unwrap().0.unwrap();
13970
13971        let mut frontend = FrontendState::new();
13972
13973        let ctx = ExecutorContext::new_mock(None).await;
13974        let version = Version(0);
13975
13976        frontend.program = program.clone();
13977        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13978        frontend.update_state_after_exec(outcome, true);
13979        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13980        let sketch_id = sketch_object.id;
13981        let sketch = expect_sketch(sketch_object);
13982        let point_id = *sketch.segments.first().unwrap();
13983        let line_id = *sketch.segments.get(3).unwrap();
13984
13985        let constraint = Constraint::Midpoint(Midpoint {
13986            point: ConstraintSegment::from(point_id),
13987            segment: line_id,
13988        });
13989        let (src_delta, scene_delta) = frontend
13990            .add_constraint(&ctx, version, sketch_id, constraint)
13991            .await
13992            .unwrap();
13993        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13994        assert_eq!(
13995            scene_delta.new_graph.objects.len(),
13996            7,
13997            "{:#?}",
13998            scene_delta.new_graph.objects
13999        );
14000
14001        ctx.close().await;
14002    }
14003
14004    #[tokio::test(flavor = "multi_thread")]
14005    async fn test_segments_symmetric() {
14006        let initial_source = "\
14007sketch(on = XY) {
14008  line(start = [var 0, var 0], end = [var 0, var 4])
14009  line(start = [var 4, var 0], end = [var 4, var 4])
14010  line(start = [var 2, var -1], end = [var 2, var 5])
14011}
14012";
14013
14014        let program = Program::parse(initial_source).unwrap().0.unwrap();
14015
14016        let mut frontend = FrontendState::new();
14017
14018        let ctx = ExecutorContext::new_mock(None).await;
14019        let version = Version(0);
14020
14021        frontend.program = program.clone();
14022        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14023        frontend.update_state_after_exec(outcome, true);
14024        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14025        let sketch_id = sketch_object.id;
14026        let sketch = expect_sketch(sketch_object);
14027        let line1_id = *sketch.segments.get(2).unwrap();
14028        let line2_id = *sketch.segments.get(5).unwrap();
14029        let axis_id = *sketch.segments.get(8).unwrap();
14030
14031        let constraint = Constraint::Symmetric(Symmetric {
14032            input: vec![line1_id, line2_id],
14033            axis: axis_id,
14034        });
14035        let (src_delta, scene_delta) = frontend
14036            .add_constraint(&ctx, version, sketch_id, constraint)
14037            .await
14038            .unwrap();
14039        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14040        assert_eq!(
14041            scene_delta.new_graph.objects.len(),
14042            12,
14043            "{:#?}",
14044            scene_delta.new_graph.objects
14045        );
14046
14047        ctx.close().await;
14048    }
14049
14050    #[tokio::test(flavor = "multi_thread")]
14051    async fn test_point_arc_midpoint() {
14052        let initial_source = "\
14053sketch(on = XY) {
14054  point(at = [var 6, var 3])
14055  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14056}
14057";
14058
14059        let program = Program::parse(initial_source).unwrap().0.unwrap();
14060
14061        let mut frontend = FrontendState::new();
14062
14063        let ctx = ExecutorContext::new_mock(None).await;
14064        let version = Version(0);
14065
14066        frontend.program = program.clone();
14067        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14068        frontend.update_state_after_exec(outcome, true);
14069        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14070        let sketch_id = sketch_object.id;
14071        let sketch = expect_sketch(sketch_object);
14072        let point_id = *sketch.segments.first().unwrap();
14073        let arc_id = *sketch.segments.get(4).unwrap();
14074
14075        let constraint = Constraint::Midpoint(Midpoint {
14076            point: ConstraintSegment::from(point_id),
14077            segment: arc_id,
14078        });
14079        let (src_delta, scene_delta) = frontend
14080            .add_constraint(&ctx, version, sketch_id, constraint)
14081            .await
14082            .unwrap();
14083        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14084        assert_eq!(
14085            scene_delta.new_graph.objects.len(),
14086            8,
14087            "{:#?}",
14088            scene_delta.new_graph.objects
14089        );
14090
14091        ctx.close().await;
14092    }
14093
14094    #[tokio::test(flavor = "multi_thread")]
14095    async fn test_origin_line_midpoint() {
14096        let initial_source = "\
14097sketch(on = XY) {
14098  line(start = [var 0, var 0], end = [var 6, var 4])
14099}
14100";
14101
14102        let program = Program::parse(initial_source).unwrap().0.unwrap();
14103
14104        let mut frontend = FrontendState::new();
14105
14106        let ctx = ExecutorContext::new_mock(None).await;
14107        let version = Version(0);
14108
14109        frontend.program = program.clone();
14110        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14111        frontend.update_state_after_exec(outcome, true);
14112        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14113        let sketch_id = sketch_object.id;
14114        let sketch = expect_sketch(sketch_object);
14115        let line_id = *sketch.segments.get(2).unwrap();
14116
14117        let constraint = Constraint::Midpoint(Midpoint {
14118            point: ConstraintSegment::ORIGIN,
14119            segment: line_id,
14120        });
14121        let (src_delta, scene_delta) = frontend
14122            .add_constraint(&ctx, version, sketch_id, constraint)
14123            .await
14124            .unwrap();
14125        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14126        assert_eq!(
14127            scene_delta.new_graph.objects.len(),
14128            6,
14129            "{:#?}",
14130            scene_delta.new_graph.objects
14131        );
14132
14133        ctx.close().await;
14134    }
14135
14136    #[tokio::test(flavor = "multi_thread")]
14137    async fn test_origin_arc_midpoint() {
14138        let initial_source = "\
14139sketch(on = XY) {
14140  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14141}
14142";
14143
14144        let program = Program::parse(initial_source).unwrap().0.unwrap();
14145
14146        let mut frontend = FrontendState::new();
14147
14148        let ctx = ExecutorContext::new_mock(None).await;
14149        let version = Version(0);
14150
14151        frontend.program = program.clone();
14152        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14153        frontend.update_state_after_exec(outcome, true);
14154        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14155        let sketch_id = sketch_object.id;
14156        let sketch = expect_sketch(sketch_object);
14157        let arc_id = *sketch.segments.get(3).unwrap();
14158
14159        let constraint = Constraint::Midpoint(Midpoint {
14160            point: ConstraintSegment::ORIGIN,
14161            segment: arc_id,
14162        });
14163        let (src_delta, scene_delta) = frontend
14164            .add_constraint(&ctx, version, sketch_id, constraint)
14165            .await
14166            .unwrap();
14167        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14168        assert_eq!(
14169            scene_delta.new_graph.objects.len(),
14170            7,
14171            "{:#?}",
14172            scene_delta.new_graph.objects
14173        );
14174
14175        ctx.close().await;
14176    }
14177
14178    #[tokio::test(flavor = "multi_thread")]
14179    async fn test_segments_symmetric_arcs() {
14180        let initial_source = "\
14181sketch(on = XY) {
14182  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14183  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14184  line(start = [var 0, var -10], end = [var 0, var 10])
14185}
14186";
14187
14188        let program = Program::parse(initial_source).unwrap().0.unwrap();
14189
14190        let mut frontend = FrontendState::new();
14191
14192        let ctx = ExecutorContext::new_mock(None).await;
14193        let version = Version(0);
14194
14195        frontend.program = program.clone();
14196        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14197        frontend.update_state_after_exec(outcome, true);
14198        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14199        let sketch_id = sketch_object.id;
14200        let sketch = expect_sketch(sketch_object);
14201        let arc1_id = *sketch.segments.get(3).unwrap();
14202        let arc2_id = *sketch.segments.get(7).unwrap();
14203        let axis_id = *sketch.segments.get(10).unwrap();
14204
14205        let constraint = Constraint::Symmetric(Symmetric {
14206            input: vec![arc1_id, arc2_id],
14207            axis: axis_id,
14208        });
14209        let (src_delta, scene_delta) = frontend
14210            .add_constraint(&ctx, version, sketch_id, constraint)
14211            .await
14212            .unwrap();
14213        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14214        assert_eq!(
14215            scene_delta.new_graph.objects.len(),
14216            14,
14217            "{:#?}",
14218            scene_delta.new_graph.objects
14219        );
14220
14221        ctx.close().await;
14222    }
14223
14224    #[tokio::test(flavor = "multi_thread")]
14225    async fn test_sketch_on_face_simple() {
14226        let initial_source = "\
14227len = 2mm
14228cube = startSketchOn(XY)
14229  |> startProfile(at = [0, 0])
14230  |> line(end = [len, 0], tag = $side)
14231  |> line(end = [0, len])
14232  |> line(end = [-len, 0])
14233  |> line(end = [0, -len])
14234  |> close()
14235  |> extrude(length = len)
14236
14237face = faceOf(cube, face = side)
14238";
14239
14240        let program = Program::parse(initial_source).unwrap().0.unwrap();
14241
14242        let mut frontend = FrontendState::new();
14243
14244        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14245        let mock_ctx = ExecutorContext::new_mock(None).await;
14246        let version = Version(0);
14247
14248        frontend.hack_set_program(&ctx, program).await.unwrap();
14249        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14250        let face_id = face_object.id;
14251
14252        let sketch_args = SketchCtor {
14253            on: Plane::Object(face_id),
14254        };
14255        let (_src_delta, scene_delta, sketch_id) = frontend
14256            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14257            .await
14258            .unwrap();
14259        assert_eq!(sketch_id, ObjectId(2));
14260        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14261        let sketch_object = &scene_delta.new_graph.objects[2];
14262        assert_eq!(sketch_object.id, ObjectId(2));
14263        assert_eq!(
14264            sketch_object.kind,
14265            ObjectKind::Sketch(Sketch {
14266                args: SketchCtor {
14267                    on: Plane::Object(face_id),
14268                },
14269                plane: face_id,
14270                segments: vec![],
14271                constraints: vec![],
14272            })
14273        );
14274        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14275
14276        ctx.close().await;
14277        mock_ctx.close().await;
14278    }
14279
14280    #[tokio::test(flavor = "multi_thread")]
14281    async fn test_new_sketch_on_primitive_index_face() {
14282        let initial_source = "\
14283@settings(kclVersion = 2.0)
14284
14285sketch001 = sketch(on = XY) {
14286  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14287}
14288extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14289shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14290        let program = Program::parse(initial_source).unwrap().0.unwrap();
14291        let ctx = ExecutorContext::new_mock(None).await;
14292        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14293        let solid_id = match outcome.variables.get("shell001") {
14294            Some(KclValueView::Solid { value }) => value.id,
14295            value => panic!("expected shell001 to be a solid, got {value:?}"),
14296        };
14297        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14298
14299        let mut ast = program.ast;
14300        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14301        let face_expr = sketch_on_ast_expr(
14302            &mut ast,
14303            &scene_graph,
14304            &solid_references,
14305            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14306        )
14307        .unwrap();
14308        let face_decl = ast::VariableDeclaration::new(
14309            ast::VariableDeclarator::new("face001", face_expr),
14310            ast::ItemVisibility::Default,
14311            ast::VariableKind::Const,
14312        );
14313        ast.body
14314            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14315                face_decl,
14316            ))));
14317        let face_source = source_from_ast(&ast);
14318        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14319        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14320
14321        let program = Program::parse(&new_source).unwrap().0.unwrap();
14322        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14323        ctx.close().await;
14324    }
14325
14326    #[tokio::test(flavor = "multi_thread")]
14327    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14328        let initial_source = "\
14329s = sketch(on = YZ) {
14330  line1 = line(start = [0, 0], end = [0, 1])
14331  line2 = line(start = [0, 1], end = [1, 1])
14332  line3 = line(start = [1, 1], end = [0, 0])
14333}
14334region001 = region(point = [0.1, 0.1], sketch = s)
14335extrude001 = extrude(region001, length = 5)
14336";
14337
14338        let program = Program::parse(initial_source).unwrap().0.unwrap();
14339
14340        let mut frontend = FrontendState::new();
14341        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14342        let version = Version(0);
14343
14344        frontend.hack_set_program(&ctx, program).await.unwrap();
14345        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14346
14347        let sketch_args = SketchCtor {
14348            on: Plane::Object(wall_object_id),
14349        };
14350        let (src_delta, _scene_delta, _sketch_id) = frontend
14351            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14352            .await
14353            .unwrap();
14354        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14355
14356        ctx.close().await;
14357    }
14358
14359    #[tokio::test(flavor = "multi_thread")]
14360    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14361        let initial_source = "\
14362sketch001 = sketch(on = YZ) {
14363  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14364  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14365  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14366  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14367  coincident([line1.end, line2.start])
14368  coincident([line2.end, line3.start])
14369  coincident([line3.end, line4.start])
14370  coincident([line4.end, line1.start])
14371  parallel([line2, line4])
14372  parallel([line3, line1])
14373  perpendicular([line1, line2])
14374  horizontal(line3)
14375  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14376}
14377region001 = region(point = [3.1, 3.74], sketch = sketch001)
14378extrude001 = extrude(region001, length = 5)
14379";
14380
14381        let program = Program::parse(initial_source).unwrap().0.unwrap();
14382
14383        let mut frontend = FrontendState::new();
14384        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14385        let version = Version(0);
14386
14387        frontend.hack_set_program(&ctx, program).await.unwrap();
14388        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14389
14390        let sketch_args = SketchCtor {
14391            on: Plane::Object(wall_object_id),
14392        };
14393        let (src_delta, _scene_delta, _sketch_id) = frontend
14394            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14395            .await
14396            .unwrap();
14397        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14398
14399        ctx.close().await;
14400    }
14401
14402    #[tokio::test(flavor = "multi_thread")]
14403    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14404        let initial_source = "\
14405@settings(kclVersion = 2.0)
14406
14407sketch001 = sketch(on = XY) {
14408  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14409  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14410}
14411hidden001 = hide(sketch001)
14412region001 = region(segments = [sketch001.circle2])
14413region002 = region(segments = [sketch001.circle1])
14414extrude001 = extrude([region001, region002], length = 5)
14415";
14416
14417        let program = Program::parse(initial_source).unwrap().0.unwrap();
14418        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14419        let version = Version(0);
14420
14421        for (solid_output_index, expected_face) in [
14422            (0, "faceOf(extrude001[0], face = END)"),
14423            (1, "faceOf(extrude001[1], face = END)"),
14424        ] {
14425            let mut frontend = FrontendState::new();
14426            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14427            let cap_object_id = find_cap_object_id_with_solid_output_index(
14428                &frontend.scene_graph,
14429                crate::frontend::api::CapKind::End,
14430                solid_output_index,
14431            )
14432            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14433
14434            let sketch_args = SketchCtor {
14435                on: Plane::Object(cap_object_id),
14436            };
14437            let (src_delta, _scene_delta, _sketch_id) = frontend
14438                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14439                .await
14440                .unwrap();
14441
14442            assert!(
14443                src_delta.text.contains(expected_face),
14444                "expected `{expected_face}` in:\n{}",
14445                src_delta.text
14446            );
14447            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14448        }
14449
14450        ctx.close().await;
14451    }
14452
14453    #[tokio::test(flavor = "multi_thread")]
14454    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14455        let initial_source = "\
14456@settings(kclVersion = 2.0)
14457
14458sketch001 = sketch(on = XY) {
14459  rect1Line1 = line(start = [0, 0], end = [1, 0])
14460  rect1Line2 = line(start = [1, 0], end = [1, 1])
14461  rect1Line3 = line(start = [1, 1], end = [0, 1])
14462  rect1Line4 = line(start = [0, 1], end = [0, 0])
14463  rect2Line1 = line(start = [3, 0], end = [4, 0])
14464  rect2Line2 = line(start = [4, 0], end = [4, 1])
14465  rect2Line3 = line(start = [4, 1], end = [3, 1])
14466  rect2Line4 = line(start = [3, 1], end = [3, 0])
14467}
14468hidden001 = hide(sketch001)
14469region001 = region(segments = [
14470  sketch001.rect1Line4,
14471  sketch001.rect1Line1
14472])
14473region002 = region(segments = [
14474  sketch001.rect2Line4,
14475  sketch001.rect2Line1
14476])
14477extrude001 = extrude([region001, region002], length = 5)
14478";
14479
14480        let program = Program::parse(initial_source).unwrap().0.unwrap();
14481        let mut frontend = FrontendState::new();
14482        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14483        let version = Version(0);
14484
14485        frontend.hack_set_program(&ctx, program).await.unwrap();
14486        let region_call = "\
14487region(segments = [
14488  sketch001.rect1Line4,
14489  sketch001.rect1Line1
14490])";
14491        let region_call_start = initial_source.find(region_call).unwrap();
14492        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14493        let segment_call = "line(start = [0, 0], end = [1, 0])";
14494        let segment_call_start = initial_source.find(segment_call).unwrap();
14495        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14496        let wall_object_id = frontend
14497            .scene_graph
14498            .objects
14499            .iter()
14500            .find_map(|object| match &object.kind {
14501                ObjectKind::Wall(wall)
14502                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14503                        && wall.source.segment.range == segment_range =>
14504                {
14505                    Some(object.id)
14506                }
14507                _ => None,
14508            })
14509            .expect("expected a wall object for region001.tags.rect1Line1");
14510
14511        let sketch_args = SketchCtor {
14512            on: Plane::Object(wall_object_id),
14513        };
14514        let (src_delta, _scene_delta, _sketch_id) = frontend
14515            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14516            .await
14517            .unwrap();
14518
14519        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14520        assert!(
14521            src_delta.text.contains(expected_face),
14522            "expected `{expected_face}` in:\n{}",
14523            src_delta.text
14524        );
14525        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14526
14527        ctx.close().await;
14528    }
14529
14530    #[test]
14531    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14532        let source = "\
14533sketch001 = sketch(on = XY) {
14534  line(start = [0, 0], end = [1, 0])
14535}
14536part = subtract(boxSolid, tools = [cutSolid])
14537  |> appearance(color = \"#8f96a3\")
14538";
14539        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14540        let line_start = source.find("line").unwrap();
14541        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14542        let line_ref = SourceRef::Simple {
14543            range: [line_start, line_end, 0].into(),
14544            node_path: None,
14545        };
14546        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14547
14548        let subtract_start = source.find("subtract").unwrap();
14549        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14550        let subtract_ref = SourceRef::Simple {
14551            range: [subtract_start, subtract_end, 0].into(),
14552            node_path: None,
14553        };
14554        assert_eq!(
14555            variable_name_containing_source_ref(&ast, &subtract_ref),
14556            Some("part".to_owned())
14557        );
14558    }
14559
14560    #[tokio::test(flavor = "multi_thread")]
14561    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14562        clear_mem_cache().await;
14563        let source = "\
14564boxSolid = startSketchOn(XY)
14565  |> startProfile(at = [0, 0])
14566  |> line(end = [4, 0], tag = $bottomEdge)
14567  |> line(end = [0, 4])
14568  |> line(end = [-4, 0])
14569  |> close()
14570  |> extrude(length = 10)
14571cutSolid = startSketchOn(XY)
14572  |> startProfile(at = [1, 1])
14573  |> line(end = [1, 0])
14574  |> line(end = [0, 1])
14575  |> line(end = [-1, 0])
14576  |> close()
14577  |> extrude(length = 10)
14578part = subtract(boxSolid, tools = [cutSolid])
14579  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14580";
14581        let program = Program::parse(source).unwrap().0.unwrap();
14582        let mut frontend = FrontendState::new();
14583        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14584        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14585            SetProgramOutcome::Success { .. } => {}
14586            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14587        }
14588
14589        let sweep_call_start = source.find("extrude").unwrap();
14590        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14591        let part_call_start = source.find("subtract").unwrap();
14592        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14593        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14594        let composite_range = [part_call_start, part_call_end, 0].into();
14595
14596        let cap_object = frontend
14597            .scene_graph
14598            .objects
14599            .iter()
14600            .find(|object| {
14601                matches!(
14602                    &object.kind,
14603                    ObjectKind::Cap(crate::frontend::api::Cap {
14604                        kind: crate::frontend::api::CapKind::End,
14605                        source,
14606                        ..
14607                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14608                )
14609            })
14610            .expect("expected end cap object to trace through subtract and original extrude");
14611
14612        let mut ast = frontend.program.ast.clone();
14613        let cap_expr = sketch_on_ast_expr(
14614            &mut ast,
14615            &frontend.scene_graph,
14616            &frontend.solid_references,
14617            &Plane::Object(cap_object.id),
14618        )
14619        .unwrap();
14620        let cap_face_decl = ast::VariableDeclaration::new(
14621            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14622            ast::ItemVisibility::Default,
14623            ast::VariableKind::Const,
14624        );
14625        ast.body
14626            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14627                cap_face_decl,
14628            ))));
14629        let generated_source = source_from_ast(&ast);
14630
14631        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14632        assert!(!generated_source.contains("faceOf(boxSolid"));
14633        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14634            panic!("expected faceOf call");
14635        };
14636        assert_eq!(call.callee.name.name, "faceOf");
14637        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14638            panic!("expected solid name");
14639        };
14640        assert_eq!(solid_name.name.name, "part");
14641        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14642            panic!("expected face name");
14643        };
14644        assert_eq!(face_name.name.name, "END");
14645
14646        ctx.close().await;
14647    }
14648
14649    #[tokio::test(flavor = "multi_thread")]
14650    async fn test_sketch_on_plane_incremental() {
14651        let initial_source = "\
14652len = 2mm
14653cube = startSketchOn(XY)
14654  |> startProfile(at = [0, 0])
14655  |> line(end = [len, 0], tag = $side)
14656  |> line(end = [0, len])
14657  |> line(end = [-len, 0])
14658  |> line(end = [0, -len])
14659  |> close()
14660  |> extrude(length = len)
14661
14662plane = planeOf(cube, face = side)
14663";
14664
14665        let program = Program::parse(initial_source).unwrap().0.unwrap();
14666
14667        let mut frontend = FrontendState::new();
14668
14669        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14670        let mock_ctx = ExecutorContext::new_mock(None).await;
14671        let version = Version(0);
14672
14673        frontend.hack_set_program(&ctx, program).await.unwrap();
14674        // Find the last plane since the first plane is the XY plane.
14675        let plane_object = frontend
14676            .scene_graph
14677            .objects
14678            .iter()
14679            .rev()
14680            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14681            .unwrap();
14682        let plane_id = plane_object.id;
14683
14684        let sketch_args = SketchCtor {
14685            on: Plane::Object(plane_id),
14686        };
14687        let (src_delta, scene_delta, sketch_id) = frontend
14688            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14689            .await
14690            .unwrap();
14691        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14692        assert_eq!(sketch_id, ObjectId(2));
14693        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14694        let sketch_object = &scene_delta.new_graph.objects[2];
14695        assert_eq!(sketch_object.id, ObjectId(2));
14696        assert_eq!(
14697            sketch_object.kind,
14698            ObjectKind::Sketch(Sketch {
14699                args: SketchCtor {
14700                    on: Plane::Object(plane_id),
14701                },
14702                plane: plane_id,
14703                segments: vec![],
14704                constraints: vec![],
14705            })
14706        );
14707        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14708
14709        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14710        assert_eq!(plane_object.id, plane_id);
14711        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14712
14713        ctx.close().await;
14714        mock_ctx.close().await;
14715    }
14716
14717    #[tokio::test(flavor = "multi_thread")]
14718    async fn test_new_sketch_uses_unique_variable_name() {
14719        let initial_source = "\
14720sketch1 = sketch(on = XY) {
14721}
14722";
14723
14724        let program = Program::parse(initial_source).unwrap().0.unwrap();
14725
14726        let mut frontend = FrontendState::new();
14727        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14728        let version = Version(0);
14729
14730        frontend.hack_set_program(&ctx, program).await.unwrap();
14731
14732        let sketch_args = SketchCtor {
14733            on: Plane::Default(PlaneName::Yz),
14734        };
14735        let (src_delta, _, _) = frontend
14736            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14737            .await
14738            .unwrap();
14739
14740        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14741
14742        ctx.close().await;
14743    }
14744
14745    #[tokio::test(flavor = "multi_thread")]
14746    async fn test_new_sketch_twice_using_same_plane() {
14747        let initial_source = "\
14748sketch1 = sketch(on = XY) {
14749}
14750";
14751
14752        let program = Program::parse(initial_source).unwrap().0.unwrap();
14753
14754        let mut frontend = FrontendState::new();
14755        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14756        let version = Version(0);
14757
14758        frontend.hack_set_program(&ctx, program).await.unwrap();
14759
14760        let sketch_args = SketchCtor {
14761            on: Plane::Default(PlaneName::Xy),
14762        };
14763        let (src_delta, _, _) = frontend
14764            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14765            .await
14766            .unwrap();
14767
14768        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14769
14770        ctx.close().await;
14771    }
14772
14773    #[tokio::test(flavor = "multi_thread")]
14774    async fn test_sketch_mode_reuses_cached_on_expression() {
14775        let initial_source = "\
14776width = 2mm
14777sketch(on = offsetPlane(XY, offset = width)) {
14778  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14779  distance([line1.start, line1.end]) == width
14780}
14781";
14782        let program = Program::parse(initial_source).unwrap().0.unwrap();
14783
14784        let mut frontend = FrontendState::new();
14785        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14786        let mock_ctx = ExecutorContext::new_mock(None).await;
14787        let version = Version(0);
14788        let project_id = ProjectId(0);
14789        let file_id = FileId(0);
14790
14791        frontend.hack_set_program(&ctx, program).await.unwrap();
14792        let initial_object_count = frontend.scene_graph.objects.len();
14793        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14794            .expect("Expected sketch object to exist")
14795            .id;
14796
14797        // Entering sketch mode should reuse cached `on` expression state
14798        // (offsetPlane result), not fail or create extra on-surface objects.
14799        let scene_delta = frontend
14800            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14801            .await
14802            .unwrap();
14803        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14804
14805        // A follow-up sketch-mode execution should keep the same stable object
14806        // graph shape as well.
14807        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14808        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14809
14810        ctx.close().await;
14811        mock_ctx.close().await;
14812    }
14813
14814    #[tokio::test(flavor = "multi_thread")]
14815    async fn test_edit_sketch_nested_in_pipe() {
14816        clear_mem_cache().await;
14817        let source = r#"
14818profile = sketch(on = XY) {
14819  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14820}
14821  |> translate(x = 2mm)
14822"#;
14823        let program = Program::parse_no_errs(source).unwrap();
14824        let mut frontend = FrontendState::new();
14825        let mock_ctx = ExecutorContext::new_mock(None).await;
14826        let version = Version(0);
14827
14828        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14829        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14830            .expect("Expected piped sketch object")
14831            .id;
14832
14833        let scene_delta = frontend
14834            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14835            .await
14836            .unwrap();
14837        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14838        assert!(
14839            scene_delta
14840                .new_graph
14841                .objects
14842                .iter()
14843                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14844            "Expected the piped sketch's segments to be present in sketch mode"
14845        );
14846
14847        clear_mem_cache().await;
14848        mock_ctx.close().await;
14849    }
14850
14851    #[tokio::test(flavor = "multi_thread")]
14852    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14853        clear_mem_cache().await;
14854        let source = r#"
14855useFirstProfile = true
14856
14857profile = if useFirstProfile {
14858  sketch(on = XY) {
14859    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14860  }
14861} else {
14862  sketch(on = XY) {
14863    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14864  }
14865}
14866"#;
14867        let program = Program::parse_no_errs(source).unwrap();
14868        let mut frontend = FrontendState::new();
14869        let mock_ctx = ExecutorContext::new_mock(None).await;
14870        let version = Version(0);
14871
14872        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14873        let sketch_object =
14874            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14875        let sketch_id = sketch_object.id;
14876        let sketch = expect_sketch(sketch_object);
14877        let line_end_id = *sketch
14878            .segments
14879            .get(1)
14880            .expect("Expected the active branch's line end point");
14881
14882        let scene_delta = frontend
14883            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14884            .await
14885            .unwrap();
14886        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14887
14888        let segments = vec![ExistingSegmentCtor {
14889            id: line_end_id,
14890            ctor: SegmentCtor::Point(PointCtor {
14891                position: Point2d {
14892                    x: Expr::Var(Number {
14893                        value: 30.0,
14894                        units: NumericSuffix::Mm,
14895                    }),
14896                    y: Expr::Var(Number {
14897                        value: 15.0,
14898                        units: NumericSuffix::Mm,
14899                    }),
14900                },
14901            }),
14902        }];
14903        let (source_delta, _) = frontend
14904            .edit_segments(&mock_ctx, version, sketch_id, segments)
14905            .await
14906            .unwrap();
14907        assert!(
14908            source_delta
14909                .text
14910                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14911            "Expected the active branch's dragged variables to be updated:\n{}",
14912            source_delta.text
14913        );
14914        assert!(
14915            source_delta
14916                .text
14917                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14918            "Expected the inactive branch to remain unchanged:\n{}",
14919            source_delta.text
14920        );
14921
14922        clear_mem_cache().await;
14923        mock_ctx.close().await;
14924    }
14925
14926    #[tokio::test(flavor = "multi_thread")]
14927    async fn test_multiple_sketch_blocks() {
14928        let initial_source = "\
14929// Cube that requires the engine.
14930width = 2
14931sketch001 = startSketchOn(XY)
14932profile001 = startProfile(sketch001, at = [0, 0])
14933  |> yLine(length = width, tag = $seg1)
14934  |> xLine(length = width)
14935  |> yLine(length = -width)
14936  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14937  |> close()
14938extrude001 = extrude(profile001, length = width)
14939
14940// Get a value that requires the engine.
14941x = segLen(seg1)
14942
14943// Triangle with side length 2*x.
14944sketch(on = XY) {
14945  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14946  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14947  coincident([line1.end, line2.start])
14948  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14949  coincident([line2.end, line3.start])
14950  coincident([line3.end, line1.start])
14951  equalLength([line3, line1])
14952  equalLength([line1, line2])
14953  distance([line1.start, line1.end]) == 2*x
14954}
14955
14956// Line segment with length x.
14957sketch2 = sketch(on = XY) {
14958  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14959  distance([line1.start, line1.end]) == x
14960}
14961";
14962
14963        let program = Program::parse(initial_source).unwrap().0.unwrap();
14964
14965        let mut frontend = FrontendState::new();
14966
14967        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14968        let mock_ctx = ExecutorContext::new_mock(None).await;
14969        let version = Version(0);
14970        let project_id = ProjectId(0);
14971        let file_id = FileId(0);
14972
14973        frontend.hack_set_program(&ctx, program).await.unwrap();
14974        let sketch_objects = frontend
14975            .scene_graph
14976            .objects
14977            .iter()
14978            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14979            .collect::<Vec<_>>();
14980        let sketch1_id = sketch_objects.first().unwrap().id;
14981        let sketch2_id = sketch_objects.get(1).unwrap().id;
14982        // First point in sketch1.
14983        let point1_id = ObjectId(sketch1_id.0 + 1);
14984        // First point in sketch2.
14985        let point2_id = ObjectId(sketch2_id.0 + 1);
14986
14987        // Edit the first sketch. Objects before the sketch block should be
14988        // present from execution cache so that we can sketch on prior planes,
14989        // for example. Objects after the first sketch block should not be
14990        // present since those statements are skipped in sketch mode.
14991        //
14992        // - startSketchOn(XY) Plane 1
14993        // - sketch on=XY Plane 1
14994        // - Sketch block 16
14995        let scene_delta = frontend
14996            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14997            .await
14998            .unwrap();
14999        assert_eq!(
15000            scene_delta.new_graph.objects.len(),
15001            18,
15002            "{:#?}",
15003            scene_delta.new_graph.objects
15004        );
15005
15006        // Edit a point in the first sketch.
15007        let point_ctor = PointCtor {
15008            position: Point2d {
15009                x: Expr::Var(Number {
15010                    value: 1.0,
15011                    units: NumericSuffix::Mm,
15012                }),
15013                y: Expr::Var(Number {
15014                    value: 2.0,
15015                    units: NumericSuffix::Mm,
15016                }),
15017            },
15018        };
15019        let segments = vec![ExistingSegmentCtor {
15020            id: point1_id,
15021            ctor: SegmentCtor::Point(point_ctor),
15022        }];
15023        let (src_delta, _) = frontend
15024            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15025            .await
15026            .unwrap();
15027        // Only the first sketch block changes.
15028        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15029        let edited_sketch1_source = src_delta.text.clone();
15030
15031        // Execute mock to simulate drag end.
15032        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15033        assert_eq!(src_delta.text, edited_sketch1_source);
15034        // Exit sketch. Objects from the entire program should be present.
15035        //
15036        // - startSketchOn(XY) Plane 1
15037        // - sketch on=XY Plane 1
15038        // - Sketch block 16
15039        // - sketch on=XY cached
15040        // - Sketch block 5
15041        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15042        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15043
15044        // Edit the second sketch.
15045        //
15046        // - startSketchOn(XY) Plane 1
15047        // - sketch on=XY Plane 1
15048        // - Sketch block 16
15049        // - sketch on=XY cached
15050        // - Sketch block 5
15051        let scene_delta = frontend
15052            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15053            .await
15054            .unwrap();
15055        assert_eq!(
15056            scene_delta.new_graph.objects.len(),
15057            24,
15058            "{:#?}",
15059            scene_delta.new_graph.objects
15060        );
15061
15062        // Edit a point in the second sketch.
15063        let point_ctor = PointCtor {
15064            position: Point2d {
15065                x: Expr::Var(Number {
15066                    value: 3.0,
15067                    units: NumericSuffix::Mm,
15068                }),
15069                y: Expr::Var(Number {
15070                    value: 4.0,
15071                    units: NumericSuffix::Mm,
15072                }),
15073            },
15074        };
15075        let segments = vec![ExistingSegmentCtor {
15076            id: point2_id,
15077            ctor: SegmentCtor::Point(point_ctor),
15078        }];
15079        let (src_delta, _) = frontend
15080            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15081            .await
15082            .unwrap();
15083        // Only the second sketch block changes.
15084        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15085        let edited_sketch2_source = src_delta.text.clone();
15086
15087        // Execute mock to simulate drag end.
15088        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15089        assert_eq!(src_delta.text, edited_sketch2_source);
15090
15091        ctx.close().await;
15092        mock_ctx.close().await;
15093    }
15094
15095    #[tokio::test(flavor = "multi_thread")]
15096    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15097        clear_mem_cache().await;
15098
15099        let source = r#"sketch001 = sketch(on = XZ) {
15100  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15101}
15102sketch002 = sketch(on = XY) {
15103  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15104  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15105  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15106  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15107  coincident([line1.end, line2.start])
15108  coincident([line2.end, line3.start])
15109  coincident([line3.end, line4.start])
15110  coincident([line4.end, line1.start])
15111  parallel([line2, line4])
15112  parallel([line3, line1])
15113  perpendicular([line1, line2])
15114  horizontal(line3)
15115  coincident([line1.start, ORIGIN])
15116}
15117"#;
15118
15119        let program = Program::parse(source).unwrap().0.unwrap();
15120        let mut frontend = FrontendState::new();
15121        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15122        let mock_ctx = ExecutorContext::new_mock(None).await;
15123        let version = Version(0);
15124        let project_id = ProjectId(0);
15125        let file_id = FileId(0);
15126
15127        frontend.hack_set_program(&ctx, program).await.unwrap();
15128        let sketch_objects = frontend
15129            .scene_graph
15130            .objects
15131            .iter()
15132            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15133            .collect::<Vec<_>>();
15134        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15135
15136        let sketch1_id = sketch_objects[0].id;
15137        let sketch2_id = sketch_objects[1].id;
15138
15139        frontend
15140            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15141            .await
15142            .unwrap();
15143        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15144
15145        let scene_delta = frontend
15146            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15147            .await
15148            .unwrap();
15149        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15150
15151        clear_mem_cache().await;
15152        ctx.close().await;
15153        mock_ctx.close().await;
15154    }
15155
15156    // Regression tests: operations on source code with extra whitespace/newlines.
15157    // These test that NodePath-based lookups work correctly when source ranges
15158    // are shifted by extra whitespace that wouldn't be present after formatting.
15159
15160    #[tokio::test(flavor = "multi_thread")]
15161    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15162        // Extra newlines after @settings line - this shifts all source ranges.
15163        let initial_source = "@settings(defaultLengthUnit = mm)
15164
15165sketch001 = sketch(on = XY) {
15166  point(at = [1in, 2in])
15167}
15168";
15169
15170        let program = Program::parse(initial_source).unwrap().0.unwrap();
15171        let mut frontend = FrontendState::new();
15172
15173        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15174        let mock_ctx = ExecutorContext::new_mock(None).await;
15175        let version = Version(0);
15176        let project_id = ProjectId(0);
15177        let file_id = FileId(0);
15178
15179        frontend.hack_set_program(&ctx, program).await.unwrap();
15180        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15181        let sketch_id = sketch_object.id;
15182
15183        // Edit sketch should succeed despite extra newlines.
15184        frontend
15185            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15186            .await
15187            .unwrap();
15188
15189        // Add a new point to the sketch.
15190        let point_ctor = PointCtor {
15191            position: Point2d {
15192                x: Expr::Number(Number {
15193                    value: 5.0,
15194                    units: NumericSuffix::Mm,
15195                }),
15196                y: Expr::Number(Number {
15197                    value: 6.0,
15198                    units: NumericSuffix::Mm,
15199                }),
15200            },
15201        };
15202        let segment = SegmentCtor::Point(point_ctor);
15203        let (src_delta, scene_delta) = frontend
15204            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15205            .await
15206            .unwrap();
15207        // After adding a point, the source should be reformatted with standard whitespace.
15208        assert!(
15209            src_delta.text.contains("point(at = [5mm, 6mm])"),
15210            "Expected new point in source, got: {}",
15211            src_delta.text
15212        );
15213        assert!(!scene_delta.new_objects.is_empty());
15214
15215        ctx.close().await;
15216        mock_ctx.close().await;
15217    }
15218
15219    #[tokio::test(flavor = "multi_thread")]
15220    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15221        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15222
15223        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15224        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15225
15226        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15227        assert!(
15228            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15229            "Expected experimental settings to be added to source"
15230        );
15231    }
15232
15233    #[tokio::test(flavor = "multi_thread")]
15234    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15235        // Extra newlines after @settings, with an empty sketch block.
15236        let initial_source = "@settings(defaultLengthUnit = mm)
15237
15238s = sketch(on = XY) {}
15239";
15240
15241        let program = Program::parse(initial_source).unwrap().0.unwrap();
15242        let mut frontend = FrontendState::new();
15243
15244        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15245        let mock_ctx = ExecutorContext::new_mock(None).await;
15246        let version = Version(0);
15247
15248        frontend.hack_set_program(&ctx, program).await.unwrap();
15249        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15250        let sketch_id = sketch_object.id;
15251
15252        let line_ctor = LineCtor {
15253            start: Point2d {
15254                x: Expr::Number(Number {
15255                    value: 0.0,
15256                    units: NumericSuffix::Mm,
15257                }),
15258                y: Expr::Number(Number {
15259                    value: 0.0,
15260                    units: NumericSuffix::Mm,
15261                }),
15262            },
15263            end: Point2d {
15264                x: Expr::Number(Number {
15265                    value: 10.0,
15266                    units: NumericSuffix::Mm,
15267                }),
15268                y: Expr::Number(Number {
15269                    value: 10.0,
15270                    units: NumericSuffix::Mm,
15271                }),
15272            },
15273            construction: None,
15274        };
15275        let segment = SegmentCtor::Line(line_ctor);
15276        let (src_delta, scene_delta) = frontend
15277            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15278            .await
15279            .unwrap();
15280        assert!(
15281            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15282            "Expected line in source, got: {}",
15283            src_delta.text
15284        );
15285        // Line creates start point, end point, and line segment.
15286        assert_eq!(scene_delta.new_objects.len(), 3);
15287
15288        ctx.close().await;
15289        mock_ctx.close().await;
15290    }
15291
15292    #[tokio::test(flavor = "multi_thread")]
15293    async fn test_extra_newlines_between_operations_edit_line() {
15294        // Extra newlines between @settings and sketch, and inside the sketch block.
15295        let initial_source = "@settings(defaultLengthUnit = mm)
15296
15297sketch001 = sketch(on = XY) {
15298
15299  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15300
15301}
15302";
15303
15304        let program = Program::parse(initial_source).unwrap().0.unwrap();
15305        let mut frontend = FrontendState::new();
15306
15307        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15308        let mock_ctx = ExecutorContext::new_mock(None).await;
15309        let version = Version(0);
15310        let project_id = ProjectId(0);
15311        let file_id = FileId(0);
15312
15313        frontend.hack_set_program(&ctx, program).await.unwrap();
15314        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15315        let sketch_id = sketch_object.id;
15316        let sketch = expect_sketch(sketch_object);
15317
15318        // Extract segment IDs before edit_sketch borrows frontend mutably.
15319        let line_id = sketch
15320            .segments
15321            .iter()
15322            .copied()
15323            .find(|seg_id| {
15324                matches!(
15325                    &frontend.scene_graph.objects[seg_id.0].kind,
15326                    ObjectKind::Segment {
15327                        segment: Segment::Line(_)
15328                    }
15329                )
15330            })
15331            .expect("Expected a line segment in sketch");
15332
15333        // Enter sketch edit mode.
15334        frontend
15335            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15336            .await
15337            .unwrap();
15338
15339        // Edit the line.
15340        let line_ctor = LineCtor {
15341            start: Point2d {
15342                x: Expr::Var(Number {
15343                    value: 1.0,
15344                    units: NumericSuffix::Mm,
15345                }),
15346                y: Expr::Var(Number {
15347                    value: 2.0,
15348                    units: NumericSuffix::Mm,
15349                }),
15350            },
15351            end: Point2d {
15352                x: Expr::Var(Number {
15353                    value: 13.0,
15354                    units: NumericSuffix::Mm,
15355                }),
15356                y: Expr::Var(Number {
15357                    value: 14.0,
15358                    units: NumericSuffix::Mm,
15359                }),
15360            },
15361            construction: None,
15362        };
15363        let segments = vec![ExistingSegmentCtor {
15364            id: line_id,
15365            ctor: SegmentCtor::Line(line_ctor),
15366        }];
15367        let (src_delta, _scene_delta) = frontend
15368            .edit_segments(&mock_ctx, version, sketch_id, segments)
15369            .await
15370            .unwrap();
15371        assert!(
15372            src_delta
15373                .text
15374                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15375            "Expected edited line in source, got: {}",
15376            src_delta.text
15377        );
15378
15379        ctx.close().await;
15380        mock_ctx.close().await;
15381    }
15382
15383    #[tokio::test(flavor = "multi_thread")]
15384    async fn test_extra_newlines_delete_segment() {
15385        // Extra whitespace before and after the sketch block.
15386        let initial_source = "@settings(defaultLengthUnit = mm)
15387
15388sketch001 = sketch(on = XY) {
15389  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15390}
15391";
15392
15393        let program = Program::parse(initial_source).unwrap().0.unwrap();
15394        let mut frontend = FrontendState::new();
15395
15396        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15397        let mock_ctx = ExecutorContext::new_mock(None).await;
15398        let version = Version(0);
15399
15400        frontend.hack_set_program(&ctx, program).await.unwrap();
15401        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15402        let sketch_id = sketch_object.id;
15403        let sketch = expect_sketch(sketch_object);
15404
15405        // The sketch should have 3 segments: start point, center point, and the circle.
15406        assert_eq!(sketch.segments.len(), 3);
15407        let circle_id = sketch.segments[2];
15408
15409        // Delete the circle despite extra newlines in original source.
15410        let (src_delta, scene_delta) = frontend
15411            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15412            .await
15413            .unwrap();
15414        assert!(
15415            src_delta.text.contains("sketch(on = XY) {"),
15416            "Expected sketch block in source, got: {}",
15417            src_delta.text
15418        );
15419        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15420        let new_sketch = expect_sketch(new_sketch_object);
15421        assert_eq!(new_sketch.segments.len(), 0);
15422
15423        ctx.close().await;
15424        mock_ctx.close().await;
15425    }
15426
15427    #[tokio::test(flavor = "multi_thread")]
15428    async fn test_unformatted_source_add_arc() {
15429        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15430        let initial_source = "@settings(defaultLengthUnit = mm)
15431
15432sketch001 = sketch(on = XY) {
15433}
15434";
15435
15436        let program = Program::parse(initial_source).unwrap().0.unwrap();
15437        let mut frontend = FrontendState::new();
15438
15439        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15440        let mock_ctx = ExecutorContext::new_mock(None).await;
15441        let version = Version(0);
15442
15443        frontend.hack_set_program(&ctx, program).await.unwrap();
15444        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15445        let sketch_id = sketch_object.id;
15446
15447        let arc_ctor = ArcCtor {
15448            start: Point2d {
15449                x: Expr::Var(Number {
15450                    value: 5.0,
15451                    units: NumericSuffix::Mm,
15452                }),
15453                y: Expr::Var(Number {
15454                    value: 0.0,
15455                    units: NumericSuffix::Mm,
15456                }),
15457            },
15458            end: Point2d {
15459                x: Expr::Var(Number {
15460                    value: 0.0,
15461                    units: NumericSuffix::Mm,
15462                }),
15463                y: Expr::Var(Number {
15464                    value: 5.0,
15465                    units: NumericSuffix::Mm,
15466                }),
15467            },
15468            center: Point2d {
15469                x: Expr::Var(Number {
15470                    value: 0.0,
15471                    units: NumericSuffix::Mm,
15472                }),
15473                y: Expr::Var(Number {
15474                    value: 0.0,
15475                    units: NumericSuffix::Mm,
15476                }),
15477            },
15478            direction: None,
15479            construction: None,
15480        };
15481        let segment = SegmentCtor::Arc(arc_ctor);
15482        let (src_delta, scene_delta) = frontend
15483            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15484            .await
15485            .unwrap();
15486        assert!(
15487            src_delta
15488                .text
15489                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15490            "Expected arc in source, got: {}",
15491            src_delta.text
15492        );
15493        assert!(!scene_delta.new_objects.is_empty());
15494
15495        ctx.close().await;
15496        mock_ctx.close().await;
15497    }
15498
15499    #[tokio::test(flavor = "multi_thread")]
15500    async fn test_arc_direction_flows_to_source() {
15501        let initial_source = "@settings(defaultLengthUnit = mm)
15502
15503sketch001 = sketch(on = XY) {
15504}
15505";
15506
15507        let program = Program::parse(initial_source).unwrap().0.unwrap();
15508        let mut frontend = FrontendState::new();
15509
15510        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15511        let mock_ctx = ExecutorContext::new_mock(None).await;
15512        let version = Version(0);
15513
15514        frontend.hack_set_program(&ctx, program).await.unwrap();
15515        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15516        let sketch_id = sketch_object.id;
15517
15518        let point = |x: f64, y: f64| Point2d {
15519            x: Expr::Var(Number {
15520                value: x,
15521                units: NumericSuffix::Mm,
15522            }),
15523            y: Expr::Var(Number {
15524                value: y,
15525                units: NumericSuffix::Mm,
15526            }),
15527        };
15528
15529        // Adding a clockwise arc writes its direction to the source.
15530        let arc_ctor = ArcCtor {
15531            start: point(5.0, 0.0),
15532            end: point(0.0, 5.0),
15533            center: point(0.0, 0.0),
15534            direction: Some(ArcDirection::Cw),
15535            construction: None,
15536        };
15537        let (src_delta, scene_delta) = frontend
15538            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15539            .await
15540            .unwrap();
15541        assert!(
15542            src_delta.text.contains("direction = CW"),
15543            "Expected direction = CW in source, got: {}",
15544            src_delta.text
15545        );
15546        // The new objects are the end points, the center, and then the arc.
15547        let arc_id = *scene_delta.new_objects.last().unwrap();
15548
15549        // Editing the arc's points preserves the clockwise direction. The
15550        // edited points keep the same distance to the center so that the
15551        // solver doesn't need to move anything.
15552        let edited_ctor = ArcCtor {
15553            start: point(0.0, -5.0),
15554            end: point(0.0, 5.0),
15555            center: point(0.0, 0.0),
15556            direction: Some(ArcDirection::Cw),
15557            construction: None,
15558        };
15559        let (src_delta, _scene_delta) = frontend
15560            .edit_segments(
15561                &mock_ctx,
15562                version,
15563                sketch_id,
15564                vec![ExistingSegmentCtor {
15565                    id: arc_id,
15566                    ctor: SegmentCtor::Arc(edited_ctor),
15567                }],
15568            )
15569            .await
15570            .unwrap();
15571        assert!(
15572            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15573            "Expected edited start point in source, got: {}",
15574            src_delta.text
15575        );
15576        assert!(
15577            src_delta.text.contains("direction = CW"),
15578            "Expected direction = CW to be preserved in source, got: {}",
15579            src_delta.text
15580        );
15581
15582        // Editing the arc back to counterclockwise removes the direction
15583        // argument since counterclockwise is the default.
15584        let edited_ctor = ArcCtor {
15585            start: point(0.0, -5.0),
15586            end: point(0.0, 5.0),
15587            center: point(0.0, 0.0),
15588            direction: Some(ArcDirection::Ccw),
15589            construction: None,
15590        };
15591        let (src_delta, _scene_delta) = frontend
15592            .edit_segments(
15593                &mock_ctx,
15594                version,
15595                sketch_id,
15596                vec![ExistingSegmentCtor {
15597                    id: arc_id,
15598                    ctor: SegmentCtor::Arc(edited_ctor),
15599                }],
15600            )
15601            .await
15602            .unwrap();
15603        assert!(
15604            !src_delta.text.contains("direction"),
15605            "Expected direction argument to be removed from source, got: {}",
15606            src_delta.text
15607        );
15608
15609        ctx.close().await;
15610        mock_ctx.close().await;
15611    }
15612
15613    #[tokio::test(flavor = "multi_thread")]
15614    async fn test_extra_newlines_add_circle() {
15615        // Extra blank lines between settings and sketch.
15616        let initial_source = "@settings(defaultLengthUnit = mm)
15617
15618sketch001 = sketch(on = XY) {
15619}
15620";
15621
15622        let program = Program::parse(initial_source).unwrap().0.unwrap();
15623        let mut frontend = FrontendState::new();
15624
15625        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15626        let mock_ctx = ExecutorContext::new_mock(None).await;
15627        let version = Version(0);
15628
15629        frontend.hack_set_program(&ctx, program).await.unwrap();
15630        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15631        let sketch_id = sketch_object.id;
15632
15633        let circle_ctor = CircleCtor {
15634            start: Point2d {
15635                x: Expr::Var(Number {
15636                    value: 5.0,
15637                    units: NumericSuffix::Mm,
15638                }),
15639                y: Expr::Var(Number {
15640                    value: 0.0,
15641                    units: NumericSuffix::Mm,
15642                }),
15643            },
15644            center: Point2d {
15645                x: Expr::Var(Number {
15646                    value: 0.0,
15647                    units: NumericSuffix::Mm,
15648                }),
15649                y: Expr::Var(Number {
15650                    value: 0.0,
15651                    units: NumericSuffix::Mm,
15652                }),
15653            },
15654            construction: None,
15655        };
15656        let segment = SegmentCtor::Circle(circle_ctor);
15657        let (src_delta, scene_delta) = frontend
15658            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15659            .await
15660            .unwrap();
15661        assert!(
15662            src_delta
15663                .text
15664                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15665            "Expected circle in source, got: {}",
15666            src_delta.text
15667        );
15668        assert!(!scene_delta.new_objects.is_empty());
15669
15670        ctx.close().await;
15671        mock_ctx.close().await;
15672    }
15673
15674    #[tokio::test(flavor = "multi_thread")]
15675    async fn test_extra_newlines_add_constraint() {
15676        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15677        let initial_source = "@settings(defaultLengthUnit = mm)
15678
15679sketch001 = sketch(on = XY) {
15680  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15681  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15682}
15683";
15684
15685        let program = Program::parse(initial_source).unwrap().0.unwrap();
15686        let mut frontend = FrontendState::new();
15687
15688        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15689        let mock_ctx = ExecutorContext::new_mock(None).await;
15690        let version = Version(0);
15691        let project_id = ProjectId(0);
15692        let file_id = FileId(0);
15693
15694        frontend.hack_set_program(&ctx, program).await.unwrap();
15695        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15696        let sketch_id = sketch_object.id;
15697        let sketch = expect_sketch(sketch_object);
15698
15699        // Extract segment data before edit_sketch borrows frontend mutably.
15700        let line_ids: Vec<ObjectId> = sketch
15701            .segments
15702            .iter()
15703            .copied()
15704            .filter(|seg_id| {
15705                matches!(
15706                    &frontend.scene_graph.objects[seg_id.0].kind,
15707                    ObjectKind::Segment {
15708                        segment: Segment::Line(_)
15709                    }
15710                )
15711            })
15712            .collect();
15713        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15714
15715        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15716        let ObjectKind::Segment {
15717            segment: Segment::Line(line1_data),
15718        } = &line1.kind
15719        else {
15720            panic!("Expected line");
15721        };
15722        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15723        let ObjectKind::Segment {
15724            segment: Segment::Line(line2_data),
15725        } = &line2.kind
15726        else {
15727            panic!("Expected line");
15728        };
15729
15730        // Build constraint before entering sketch mode.
15731        let constraint = Constraint::Coincident(Coincident {
15732            segments: vec![line1_data.end.into(), line2_data.start.into()],
15733        });
15734
15735        // Enter sketch edit mode.
15736        frontend
15737            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15738            .await
15739            .unwrap();
15740        let (src_delta, _scene_delta) = frontend
15741            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15742            .await
15743            .unwrap();
15744        assert!(
15745            src_delta.text.contains("coincident("),
15746            "Expected coincident constraint in source, got: {}",
15747            src_delta.text
15748        );
15749
15750        ctx.close().await;
15751        mock_ctx.close().await;
15752    }
15753
15754    #[tokio::test(flavor = "multi_thread")]
15755    async fn test_extra_newlines_add_line_then_edit_line() {
15756        // Extra newlines after @settings - add a line, then edit it.
15757        let initial_source = "@settings(defaultLengthUnit = mm)
15758
15759sketch001 = sketch(on = XY) {
15760}
15761";
15762
15763        let program = Program::parse(initial_source).unwrap().0.unwrap();
15764        let mut frontend = FrontendState::new();
15765
15766        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15767        let mock_ctx = ExecutorContext::new_mock(None).await;
15768        let version = Version(0);
15769
15770        frontend.hack_set_program(&ctx, program).await.unwrap();
15771        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15772        let sketch_id = sketch_object.id;
15773
15774        // Add a line.
15775        let line_ctor = LineCtor {
15776            start: Point2d {
15777                x: Expr::Number(Number {
15778                    value: 0.0,
15779                    units: NumericSuffix::Mm,
15780                }),
15781                y: Expr::Number(Number {
15782                    value: 0.0,
15783                    units: NumericSuffix::Mm,
15784                }),
15785            },
15786            end: Point2d {
15787                x: Expr::Number(Number {
15788                    value: 10.0,
15789                    units: NumericSuffix::Mm,
15790                }),
15791                y: Expr::Number(Number {
15792                    value: 10.0,
15793                    units: NumericSuffix::Mm,
15794                }),
15795            },
15796            construction: None,
15797        };
15798        let segment = SegmentCtor::Line(line_ctor);
15799        let (src_delta, scene_delta) = frontend
15800            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15801            .await
15802            .unwrap();
15803        assert!(
15804            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15805            "Expected line in source after add, got: {}",
15806            src_delta.text
15807        );
15808        // Line creates start point, end point, and line segment.
15809        let line_id = *scene_delta.new_objects.last().unwrap();
15810
15811        // Edit the line.
15812        let line_ctor = LineCtor {
15813            start: Point2d {
15814                x: Expr::Number(Number {
15815                    value: 1.0,
15816                    units: NumericSuffix::Mm,
15817                }),
15818                y: Expr::Number(Number {
15819                    value: 2.0,
15820                    units: NumericSuffix::Mm,
15821                }),
15822            },
15823            end: Point2d {
15824                x: Expr::Number(Number {
15825                    value: 13.0,
15826                    units: NumericSuffix::Mm,
15827                }),
15828                y: Expr::Number(Number {
15829                    value: 14.0,
15830                    units: NumericSuffix::Mm,
15831                }),
15832            },
15833            construction: None,
15834        };
15835        let segments = vec![ExistingSegmentCtor {
15836            id: line_id,
15837            ctor: SegmentCtor::Line(line_ctor),
15838        }];
15839        let (src_delta, scene_delta) = frontend
15840            .edit_segments(&mock_ctx, version, sketch_id, segments)
15841            .await
15842            .unwrap();
15843        assert!(
15844            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15845            "Expected edited line in source, got: {}",
15846            src_delta.text
15847        );
15848        assert_eq!(scene_delta.new_objects, vec![]);
15849
15850        ctx.close().await;
15851        mock_ctx.close().await;
15852    }
15853
15854    #[test]
15855    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15856        // A non-target sketch block appears before the target so that the
15857        // traversal enters and leaves it before reaching the target. The
15858        // generated name must come from the top-level scope, where foo1 is
15859        // taken, not the sketch's scope, where no foo names are taken. This
15860        // is a regression test: dfs_mut used to visit the sketch block twice,
15861        // pushing its scope twice but popping it once, leaving the sketch
15862        // scope on top of the defined-names stack for the rest of the
15863        // traversal.
15864        let code = "\
15865foo1 = 1
15866sk = sketch() {
15867  p = var 1.5
15868}
158697 + 8
15870";
15871        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15872        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15873            panic!("expected an expression statement");
15874        };
15875        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15876        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15877            &mut ast,
15878            &source_ref,
15879            AstMutateCommand::AddVariableDeclaration {
15880                prefix: "foo".to_owned(),
15881            },
15882        )
15883        .unwrap();
15884        let AstMutateCommandReturn::Name(name) = cmd_return else {
15885            panic!("expected a generated name");
15886        };
15887        assert_eq!(name, "foo2");
15888        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15889            panic!("expected the expression statement to become a variable declaration");
15890        };
15891        assert_eq!(decl.name(), "foo2");
15892    }
15893
15894    /// Get the function body of the variable declaration at `ast.body[index]`.
15895    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15896        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15897            panic!("expected a variable declaration");
15898        };
15899        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15900            panic!("expected a function expression");
15901        };
15902        &func.body
15903    }
15904
15905    /// Get the then-branch block of the if-expression initializing the
15906    /// variable declaration at `ast.body[index]`.
15907    fn then_block_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::IfExpression(if_expr) = &decl.declaration.init else {
15912            panic!("expected an if expression");
15913        };
15914        &if_expr.then_val
15915    }
15916
15917    #[test]
15918    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15919        // The generated name must come from the function body's scope, where
15920        // thing1 is taken. Before dfs_mut visited function bodies as program
15921        // nodes, the top-level scope was used instead, generating thing1 and
15922        // colliding with the local.
15923        let code = "\
15924fn build() {
15925  thing1 = 1
15926  10 + 20
15927  return thing1
15928}
15929";
15930        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15931        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15932            panic!("expected an expression statement");
15933        };
15934        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15935        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15936            &mut ast,
15937            &source_ref,
15938            AstMutateCommand::AddVariableDeclaration {
15939                prefix: "thing".to_owned(),
15940            },
15941        )
15942        .unwrap();
15943        let AstMutateCommandReturn::Name(name) = cmd_return else {
15944            panic!("expected a generated name");
15945        };
15946        assert_eq!(name, "thing2");
15947        let body = &function_body_at(&ast, 0).body;
15948        assert_eq!(body.len(), 3);
15949        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15950            panic!("expected the expression statement to become a variable declaration");
15951        };
15952        assert_eq!(decl.name(), "thing2");
15953        // Siblings are untouched.
15954        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15955            panic!("expected a variable declaration");
15956        };
15957        assert_eq!(first.name(), "thing1");
15958        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15959    }
15960
15961    #[test]
15962    fn test_delete_node_in_function_body_preserves_leading_comment() {
15963        // Before the shared body traversal, MutateBodyItem::Delete was
15964        // silently dropped inside function bodies, so this reported success
15965        // without deleting anything.
15966        let code = "\
15967fn build() {
15968  a = 1
15969  // keep me
15970  b = 2
15971  return a
15972}
15973";
15974        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15975        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15976            panic!("expected a variable declaration");
15977        };
15978        assert_eq!(b_decl.name(), "b");
15979        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15980        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15981        let body = &function_body_at(&ast, 0).body;
15982        assert_eq!(body.len(), 2, "expected b to be deleted");
15983        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15984            panic!("expected a variable declaration");
15985        };
15986        assert_eq!(first.name(), "a");
15987        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15988            panic!("expected the return statement to remain");
15989        };
15990        assert!(
15991            body[1].get_comments().iter().any(|c| c.contains("keep me")),
15992            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15993            body[1].get_comments()
15994        );
15995    }
15996
15997    #[test]
15998    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
15999        // Locals in the function body are avoided: thing1 is taken, so the
16000        // generated name is thing2. But find_defined_names only sees the
16001        // block's body items, not the function's parameters, so the generated
16002        // name collides with the thing2 parameter. This pins the current
16003        // behavior.
16004        // TODO: Should function parameters be included in the scope used for
16005        // name generation?
16006        let code = "\
16007fn build(thing2) {
16008  thing1 = 1
16009  10 + 20
16010  return thing1 + thing2
16011}
16012";
16013        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16014        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16015            panic!("expected an expression statement");
16016        };
16017        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16018        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16019            &mut ast,
16020            &source_ref,
16021            AstMutateCommand::AddVariableDeclaration {
16022                prefix: "thing".to_owned(),
16023            },
16024        )
16025        .unwrap();
16026        let AstMutateCommandReturn::Name(name) = cmd_return else {
16027            panic!("expected a generated name");
16028        };
16029        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16030    }
16031
16032    #[test]
16033    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16034        let code = "\
16035x = 1
16036y = if x > 0 {
16037  q1 = 1
16038  foo(q1)
16039  q1
16040} else {
16041  2
16042}
16043";
16044        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16045        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16046            panic!("expected an expression statement");
16047        };
16048        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16049        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16050            &mut ast,
16051            &source_ref,
16052            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16053        )
16054        .unwrap();
16055        let AstMutateCommandReturn::Name(name) = cmd_return else {
16056            panic!("expected a generated name");
16057        };
16058        assert_eq!(name, "q2");
16059        let body = &then_block_at(&ast, 1).body;
16060        assert_eq!(body.len(), 3);
16061        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16062            panic!("expected the expression statement to become a variable declaration");
16063        };
16064        assert_eq!(decl.name(), "q2");
16065        // Siblings are untouched.
16066        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16067            panic!("expected a variable declaration");
16068        };
16069        assert_eq!(first.name(), "q1");
16070        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16071    }
16072
16073    #[test]
16074    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16075        // Before the shared body traversal, MutateBodyItem::Delete was
16076        // silently dropped inside if-expression branch blocks.
16077        let code = "\
16078y = if true {
16079  a = 1
16080  // keep me
16081  b = 2
16082  a + b
16083} else {
16084  2
16085}
16086";
16087        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16088        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16089            panic!("expected a variable declaration");
16090        };
16091        assert_eq!(b_decl.name(), "b");
16092        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16093        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16094        let body = &then_block_at(&ast, 0).body;
16095        assert_eq!(body.len(), 2, "expected b to be deleted");
16096        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16097            panic!("expected a variable declaration");
16098        };
16099        assert_eq!(first.name(), "a");
16100        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16101            panic!("expected the tail expression to remain");
16102        };
16103        assert!(
16104            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16105            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16106            body[1].get_comments()
16107        );
16108    }
16109}