Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::Angle;
9use kcl_api::ArcCtor;
10use kcl_api::ArcDirection;
11use kcl_api::CapSource;
12use kcl_api::CircleCtor;
13use kcl_api::Coincident;
14use kcl_api::Constraint;
15use kcl_api::ConstraintSegment;
16use kcl_api::ControlPointSplineCtor;
17use kcl_api::Diameter;
18use kcl_api::Distance;
19use kcl_api::EqualRadius;
20use kcl_api::Expr;
21use kcl_api::FixedPoint;
22use kcl_api::Freedom;
23use kcl_api::Horizontal;
24use kcl_api::LineCtor;
25use kcl_api::LinesEqualLength;
26use kcl_api::Midpoint;
27use kcl_api::Number;
28use kcl_api::NumericSuffix;
29use kcl_api::Object;
30use kcl_api::ObjectId;
31use kcl_api::ObjectKind;
32use kcl_api::Parallel;
33use kcl_api::Perpendicular;
34use kcl_api::Plane;
35use kcl_api::Point2d;
36use kcl_api::PointCtor;
37use kcl_api::Radius;
38use kcl_api::Segment;
39use kcl_api::SegmentCtor;
40use kcl_api::SketchCtor;
41use kcl_api::SourceRef;
42use kcl_api::SourceRefRange;
43use kcl_api::Symmetric;
44use kcl_api::Tangent;
45use kcl_api::UnitLength;
46use kcl_api::Vertical;
47use kcl_api::WallSource;
48use kcl_error::CompilationIssue;
49use kcl_error::SourceRange;
50use serde::Serialize;
51use uuid::Uuid;
52
53use crate::ExecOutcome;
54use crate::ExecutorContext;
55use crate::KclError;
56use crate::KclErrorWithOutputs;
57use crate::KclValueView;
58use crate::Program;
59use crate::SegmentDragAnchor;
60use crate::collections::AhashIndexSet;
61use crate::execution::Artifact;
62use crate::execution::ArtifactGraph;
63use crate::execution::ArtifactId;
64use crate::execution::CapSubType;
65use crate::execution::CodeRef;
66use crate::execution::MockConfig;
67use crate::execution::SKETCH_BLOCK_PARAM_ON;
68use crate::execution::annotations::WarningLevel;
69use crate::execution::cache::SketchModeState;
70use crate::execution::types::adjust_length;
71use crate::fmt::format_number_literal;
72use crate::front::CoincidentExt;
73use crate::front::DistanceExt;
74use crate::front::Error;
75use crate::front::ExecResult;
76use crate::front::ObjectKindExt;
77use crate::front::SegmentExt;
78use crate::frontend::api::FileId;
79use crate::frontend::api::ProjectId;
80use crate::frontend::api::RestoreSketchCheckpointOutcome;
81use crate::frontend::api::SceneGraph;
82use crate::frontend::api::SceneGraphDelta;
83use crate::frontend::api::SketchCheckpointId;
84use crate::frontend::api::SourceDelta;
85use crate::frontend::api::Version;
86use crate::frontend::modify::find_defined_names;
87use crate::frontend::modify::next_free_name;
88use crate::frontend::modify::next_free_name_with_padding;
89use crate::frontend::sketch::ConstraintLabelPositionEdit;
90use crate::frontend::sketch::ExistingSegmentCtor;
91use crate::frontend::sketch::SketchApi;
92use crate::id::IncIdGenerator;
93use crate::parsing::ast::types as ast;
94use crate::parsing::ast::types::BoxNode;
95use crate::parsing::ast::types::CallExpressionKw;
96use crate::parsing::ast::types::NodePathExt;
97use crate::std::constraints::LinesAtAngleKind;
98use crate::walk::NodeMut;
99use crate::walk::Visitable;
100use crate::walk::traverse::MutateBodyItem;
101use crate::walk::traverse::TraversalReturn;
102use crate::walk::traverse::Visitor;
103use crate::walk::traverse::dfs_mut;
104
105pub(crate) mod api;
106pub(crate) mod modify;
107pub(crate) mod sketch;
108
109pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
110
111#[derive(Debug, Clone)]
112struct SketchCheckpoint {
113    id: SketchCheckpointId,
114    source: SourceDelta,
115    program: Program,
116    scene_graph: SceneGraph,
117    exec_outcome: ExecOutcome,
118    point_freedom_cache: HashMap<ObjectId, Freedom>,
119    mock_memory: Option<SketchModeState>,
120}
121pub(crate) mod trim;
122
123struct ArcSizeConstraintParams {
124    points: Vec<ObjectId>,
125    function_name: &'static str,
126    value: f64,
127    units: NumericSuffix,
128    label_position: Option<Point2d<Number>>,
129    constraint_type_name: &'static str,
130}
131
132const POINT_FN: &str = "point";
133const POINT_AT_PARAM: &str = "at";
134const LINE_FN: &str = "line";
135const LINE_VARIABLE: &str = "line";
136const LINE_START_PARAM: &str = "start";
137const LINE_END_PARAM: &str = "end";
138const ARC_FN: &str = "arc";
139const ARC_VARIABLE: &str = "arc";
140const ARC_START_PARAM: &str = "start";
141const ARC_END_PARAM: &str = "end";
142const ARC_CENTER_PARAM: &str = "center";
143const ARC_DIRECTION_PARAM: &str = "direction";
144/// The name of the KCL std constant for a clockwise arc direction.
145const ARC_DIRECTION_CW_NAME: &str = "CW";
146const CIRCLE_FN: &str = "circle";
147const CIRCLE_VARIABLE: &str = "circle";
148const CIRCLE_START_PARAM: &str = "start";
149const CIRCLE_CENTER_PARAM: &str = "center";
150const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
151const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
152const LABEL_POSITION_PARAM: &str = "labelPosition";
153
154const COINCIDENT_FN: &str = "coincident";
155const DIAMETER_FN: &str = "diameter";
156const DISTANCE_FN: &str = "distance";
157const FIXED_FN: &str = "fixed";
158const ANGLE_FN: &str = "angle";
159const ANGLE_DIMENSION_FN: &str = "angleDimension";
160const ANGLE_LINES_PARAM: &str = "lines";
161const ANGLE_SECTOR_PARAM: &str = "sector";
162const ANGLE_INVERSE_PARAM: &str = "inverse";
163const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
164const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
165const EQUAL_LENGTH_FN: &str = "equalLength";
166const EQUAL_RADIUS_FN: &str = "equalRadius";
167const HORIZONTAL_FN: &str = "horizontal";
168const MIDPOINT_FN: &str = "midpoint";
169const MIDPOINT_POINT_PARAM: &str = "point";
170const RADIUS_FN: &str = "radius";
171const SYMMETRIC_FN: &str = "symmetric";
172const SYMMETRIC_AXIS_PARAM: &str = "axis";
173const TANGENT_FN: &str = "tangent";
174const VERTICAL_FN: &str = "vertical";
175
176const LINE_PROPERTY_START: &str = "start";
177const LINE_PROPERTY_END: &str = "end";
178
179const ARC_PROPERTY_START: &str = "start";
180const ARC_PROPERTY_END: &str = "end";
181const ARC_PROPERTY_CENTER: &str = "center";
182const CIRCLE_PROPERTY_START: &str = "start";
183const CIRCLE_PROPERTY_CENTER: &str = "center";
184const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
185const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
186
187const CONSTRUCTION_PARAM: &str = "construction";
188
189#[derive(Debug, Clone, Copy)]
190enum EditDeleteKind {
191    Edit,
192    DeleteNonSketch,
193}
194
195/// Options that control how an edit is re-executed and written back.
196struct ExecuteAfterEditOptions {
197    segment_ids_edited: AhashIndexSet<ObjectId>,
198    edit_kind: EditDeleteKind,
199    commit_solved_initial_guesses: bool,
200}
201
202impl EditDeleteKind {
203    /// Returns true if this edit is any type of deletion.
204    fn is_delete(&self) -> bool {
205        match self {
206            EditDeleteKind::Edit => false,
207            EditDeleteKind::DeleteNonSketch => true,
208        }
209    }
210
211    fn to_change_kind(self) -> ChangeKind {
212        match self {
213            EditDeleteKind::Edit => ChangeKind::Edit,
214            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
215        }
216    }
217}
218
219#[derive(Debug, Clone, Copy)]
220enum ChangeKind {
221    Add,
222    Edit,
223    Delete,
224    None,
225}
226
227#[derive(Debug, Clone, Serialize, ts_rs::TS)]
228#[ts(export, export_to = "FrontendApi.ts")]
229#[serde(tag = "type")]
230pub enum SetProgramOutcome {
231    #[serde(rename_all = "camelCase")]
232    Success {
233        scene_graph: Box<SceneGraph>,
234        exec_outcome: Box<ExecOutcome>,
235        checkpoint_id: Option<SketchCheckpointId>,
236    },
237    #[serde(rename_all = "camelCase")]
238    ExecFailure { error: Box<KclErrorWithOutputs> },
239}
240
241/// Options for a sketch segment edit that participates in drag solving.
242pub struct EditSegmentsOptions {
243    /// Narrows which edited scene objects receive temporary fixed constraints.
244    ///
245    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
246    /// disables those fixed constraints, which is useful for semantic edits such
247    /// as toggling construction state.
248    pub anchor_segment_ids: Option<Vec<ObjectId>>,
249    /// Hidden fixed cursor points that the referenced segment bodies must pass
250    /// through during solve.
251    pub drag_anchors: Vec<SegmentDragAnchor>,
252    /// Constraint label positions to write in the same AST transaction as the
253    /// segment edits. Label positions do not participate in solving.
254    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
255    /// Whether solver-updated initial guesses should be written back to KCL.
256    pub commit_solved_initial_guesses: bool,
257}
258
259/// Options for a distance-constraint label edit during sketch dragging.
260pub struct EditDistanceConstraintLabelPositionOptions {
261    /// Edited scene objects to keep anchored while previewing the label edit.
262    pub anchor_segment_ids: Vec<ObjectId>,
263    /// Whether solver-updated initial guesses should be written back to KCL.
264    pub commit_solved_initial_guesses: bool,
265}
266
267/// Options for editing a constraint during sketch dragging.
268pub struct EditConstraintOptions {
269    /// Whether solver-updated initial guesses should be written back to KCL.
270    pub commit_solved_initial_guesses: bool,
271}
272
273#[derive(Debug, Clone)]
274struct SolidAstReference {
275    variable_name: String,
276    output_index: Option<usize>,
277}
278
279#[derive(Debug, Clone)]
280pub struct FrontendState {
281    program: Program,
282    scene_graph: SceneGraph,
283    /// Lightweight map from engine solid IDs to their latest KCL references.
284    solid_references: HashMap<Uuid, SolidAstReference>,
285    /// Stores the last known freedom value for each point object.
286    /// This allows us to preserve freedom values when freedom analysis isn't run.
287    point_freedom_cache: HashMap<ObjectId, Freedom>,
288    /// One-shot drag anchors for the next segment edit. These ids define which
289    /// edited points/segments become temporary fixed constraints during solve.
290    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
291    /// One-shot segment-body drag anchors for the next segment edit. These add
292    /// a temporary solver point on the dragged segment that follows the cursor.
293    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
294    /// One-shot constraint label edits to apply with the next segment edit.
295    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
296    /// One-shot override for whether the next edit commits solver-updated
297    /// initial guesses back into KCL. Drag previews keep this off so only the
298    /// explicit drag edit feeds the next solve.
299    next_edit_commits_solver_solutions: Option<bool>,
300    sketch_checkpoints: VecDeque<SketchCheckpoint>,
301    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
302}
303
304impl Default for FrontendState {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310impl FrontendState {
311    pub fn new() -> Self {
312        Self {
313            program: Program::empty(),
314            scene_graph: SceneGraph {
315                project: ProjectId(0),
316                file: FileId(0),
317                version: Version(0),
318                objects: Default::default(),
319                settings: Default::default(),
320                sketch_mode: Default::default(),
321            },
322            solid_references: HashMap::new(),
323            point_freedom_cache: HashMap::new(),
324            next_drag_anchor_segment_ids: None,
325            next_segment_drag_anchors: None,
326            next_constraint_label_edits: None,
327            next_edit_commits_solver_solutions: None,
328            sketch_checkpoints: VecDeque::new(),
329            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
330        }
331    }
332
333    /// Get a reference to the scene graph
334    pub fn scene_graph(&self) -> &SceneGraph {
335        &self.scene_graph
336    }
337
338    pub fn default_length_unit(&self) -> UnitLength {
339        self.program
340            .meta_settings()
341            .ok()
342            .flatten()
343            .map(|settings| settings.default_length_units)
344            .unwrap_or(UnitLength::Millimeters)
345    }
346
347    pub async fn create_sketch_checkpoint(
348        &mut self,
349        cache: &crate::Cache,
350        exec_outcome: ExecOutcome,
351    ) -> api::Result<SketchCheckpointId> {
352        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
353
354        let checkpoint = SketchCheckpoint {
355            id: checkpoint_id,
356            source: SourceDelta {
357                text: source_from_ast(&self.program.ast),
358            },
359            program: self.program.clone(),
360            scene_graph: self.scene_graph.clone(),
361            exec_outcome,
362            point_freedom_cache: self.point_freedom_cache.clone(),
363            mock_memory: cache.read_old_memory().await,
364        };
365
366        self.sketch_checkpoints.push_back(checkpoint);
367        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
368            self.sketch_checkpoints.pop_front();
369        }
370
371        Ok(checkpoint_id)
372    }
373
374    /// Edit sketch segments with optional drag-solve overrides.
375    ///
376    /// Drag anchors add hidden fixed cursor points and constrain the referenced
377    /// segment bodies to pass through them, which lets body drags use the same
378    /// anchor model without pinning all child points. Preview callers disable
379    /// solver writeback so solved geometry can be returned without feeding every
380    /// solver value back into KCL.
381    pub async fn edit_segments_with_options(
382        &mut self,
383        ctx: &ExecutorContext,
384        version: Version,
385        sketch: ObjectId,
386        segments: Vec<ExistingSegmentCtor>,
387        options: EditSegmentsOptions,
388    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
389        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
390            self.next_drag_anchor_segment_ids
391                .replace(anchor_ids.into_iter().collect())
392        });
393        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
394        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
395        let previous_commit_mode = self
396            .next_edit_commits_solver_solutions
397            .replace(options.commit_solved_initial_guesses);
398        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
399        if let Some(previous_anchor_ids) = previous_anchor_ids {
400            self.next_drag_anchor_segment_ids = previous_anchor_ids;
401        }
402        self.next_segment_drag_anchors = previous_drag_anchors;
403        self.next_constraint_label_edits = previous_constraint_label_edits;
404        self.next_edit_commits_solver_solutions = previous_commit_mode;
405        result
406    }
407
408    /// Edit a distance-constraint label position with optional solver writeback.
409    ///
410    /// Drag previews set `commit_solved_initial_guesses` to false so label
411    /// placement can be previewed against solved geometry without advancing
412    /// persistent KCL state until drag completion.
413    pub async fn edit_distance_constraint_label_position_with_options(
414        &mut self,
415        ctx: &ExecutorContext,
416        version: Version,
417        sketch: ObjectId,
418        constraint_id: ObjectId,
419        label_position: Point2d<Number>,
420        options: EditDistanceConstraintLabelPositionOptions,
421    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
422        let previous_commit_mode = self
423            .next_edit_commits_solver_solutions
424            .replace(options.commit_solved_initial_guesses);
425        let result = SketchApi::edit_distance_constraint_label_position(
426            self,
427            ctx,
428            version,
429            sketch,
430            constraint_id,
431            label_position,
432            options.anchor_segment_ids,
433        )
434        .await;
435        self.next_edit_commits_solver_solutions = previous_commit_mode;
436        result
437    }
438
439    /// Edit a distance constraint with optional solver writeback.
440    pub async fn edit_distance_constraint_with_options(
441        &mut self,
442        ctx: &ExecutorContext,
443        version: Version,
444        sketch: ObjectId,
445        constraint_id: ObjectId,
446        constraint: Constraint,
447        options: EditConstraintOptions,
448    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
449        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
450            .await
451    }
452
453    /// Edit an angle constraint with optional solver writeback.
454    pub async fn edit_angle_constraint_with_options(
455        &mut self,
456        ctx: &ExecutorContext,
457        version: Version,
458        sketch: ObjectId,
459        constraint_id: ObjectId,
460        angle: Angle,
461        options: EditConstraintOptions,
462    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
463        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
464            .await
465    }
466
467    /// Edit an angle or distance-family constraint with optional solver writeback.
468    pub async fn edit_constraint_with_options(
469        &mut self,
470        ctx: &ExecutorContext,
471        _version: Version,
472        sketch: ObjectId,
473        constraint_id: ObjectId,
474        constraint: Constraint,
475        options: EditConstraintOptions,
476    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
477        // TODO: Check version.
478        let sketch_block_ref =
479            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
480
481        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
482            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
483        })?;
484
485        let mut new_ast = self.program.ast.clone();
486        let command = match &object.kind {
487            ObjectKind::Constraint {
488                constraint:
489                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
490            } => {
491                let (function_name, distance) = match &constraint {
492                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
493                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
494                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
495                    _ => {
496                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
497                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
498                        )));
499                    }
500                };
501                let (call, value) = self
502                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
503                    .map_err(KclErrorWithOutputs::no_outputs)?;
504                AstMutateCommand::EditDistanceConstraint { call, value }
505            }
506            ObjectKind::Constraint {
507                constraint: Constraint::Angle(_),
508            } => {
509                let Constraint::Angle(angle) = &constraint else {
510                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
511                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
512                    )));
513                };
514                let (call, value) = self
515                    .angle_constraint_ast_parts(angle, &mut new_ast)
516                    .map_err(KclErrorWithOutputs::no_outputs)?;
517                AstMutateCommand::EditAngleConstraint { call, value }
518            }
519            ObjectKind::Constraint { .. } => {
520                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
521                    "Editing {} is not supported",
522                    object.kind.human_friendly_kind_with_article(),
523                ))));
524            }
525            _ => {
526                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
527                    "Object is not a constraint: {constraint_id:?}"
528                ))));
529            }
530        };
531
532        self.mutate_ast(&mut new_ast, constraint_id, command)
533            .map_err(KclErrorWithOutputs::no_outputs)?;
534
535        self.execute_after_edit(
536            ctx,
537            sketch,
538            sketch_block_ref,
539            &mut new_ast,
540            ExecuteAfterEditOptions {
541                segment_ids_edited: Default::default(),
542                edit_kind: EditDeleteKind::Edit,
543                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
544            },
545        )
546        .await
547    }
548
549    pub async fn restore_sketch_checkpoint(
550        &mut self,
551        cache: &crate::Cache,
552        checkpoint_id: SketchCheckpointId,
553    ) -> api::Result<RestoreSketchCheckpointOutcome> {
554        let checkpoint = self
555            .sketch_checkpoints
556            .iter()
557            .find(|checkpoint| checkpoint.id == checkpoint_id)
558            .cloned()
559            .ok_or_else(|| Error {
560                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
561            })?;
562
563        self.program = checkpoint.program;
564        self.scene_graph = checkpoint.scene_graph.clone();
565        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
566        self.point_freedom_cache = checkpoint.point_freedom_cache;
567        self.next_drag_anchor_segment_ids = None;
568        self.next_segment_drag_anchors = None;
569        self.next_constraint_label_edits = None;
570        self.next_edit_commits_solver_solutions = None;
571
572        if let Some(mock_memory) = checkpoint.mock_memory {
573            cache.write_old_memory(mock_memory).await;
574        } else {
575            cache.clear_mem_cache().await;
576        }
577
578        Ok(RestoreSketchCheckpointOutcome {
579            source_delta: checkpoint.source,
580            scene_graph_delta: SceneGraphDelta {
581                new_graph: self.scene_graph_for_ui(),
582                new_objects: Vec::new(),
583                invalidates_ids: true,
584                // Restoring leaves the engine as it is, so report its current
585                // generation rather than the one stored with the checkpoint.
586                exec_outcome: ExecOutcome {
587                    engine_scene_generation: cache.engine_scene_generation().await,
588                    ..checkpoint.exec_outcome
589                },
590            },
591        })
592    }
593
594    pub fn clear_sketch_checkpoints(&mut self) {
595        self.sketch_checkpoints.clear();
596    }
597    fn scene_graph_for_ui(&self) -> SceneGraph {
598        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
599            matches!(
600                object.kind,
601                ObjectKind::Segment {
602                    segment: Segment::ControlPointSpline(_)
603                }
604            )
605        });
606
607        if !has_control_point_splines {
608            return self.scene_graph.clone();
609        }
610
611        let hidden_constraint_ids = self
612            .scene_graph
613            .objects
614            .iter()
615            .filter_map(|object| match &object.kind {
616                ObjectKind::Constraint {
617                    constraint: Constraint::Coincident(coincident),
618                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
619                    Some(object.id)
620                }
621                _ => None,
622            })
623            .collect::<HashSet<_>>();
624
625        if hidden_constraint_ids.is_empty() {
626            return self.scene_graph.clone();
627        }
628
629        let mut scene_graph = self.scene_graph.clone();
630        for object in &mut scene_graph.objects {
631            match &mut object.kind {
632                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
633                    object.kind = ObjectKind::Nil;
634                }
635                ObjectKind::Sketch(sketch) => {
636                    sketch
637                        .constraints
638                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
639                }
640                _ => {}
641            }
642        }
643
644        scene_graph
645    }
646}
647
648fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
649    let mut first_owner_id = None;
650    for segment_id in coincident.segment_ids() {
651        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
652            return false;
653        };
654
655        match first_owner_id {
656            Some(first_owner_id) if first_owner_id != owner_id => return false,
657            Some(_) => {}
658            None => first_owner_id = Some(owner_id),
659        }
660    }
661
662    first_owner_id.is_some()
663}
664
665fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
666    let object = scene_graph.objects.get(segment_id.0)?;
667    let ObjectKind::Segment { segment } = &object.kind else {
668        return None;
669    };
670
671    match segment {
672        Segment::ControlPointSpline(_) => Some(segment_id),
673        Segment::Point(point) => point
674            .owner
675            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
676        Segment::Line(line) => line
677            .owner
678            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
679        _ => None,
680    }
681}
682
683fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
684    matches!(
685        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
686        Some(ObjectKind::Segment {
687            segment: Segment::ControlPointSpline(_)
688        })
689    )
690}
691
692fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
693    let experimental_features_allowed = program
694        .meta_settings()
695        .ok()
696        .flatten()
697        .map(|settings| settings.experimental_features == WarningLevel::Allow)
698        .unwrap_or(false);
699    if experimental_features_allowed {
700        return Ok(program.clone());
701    }
702
703    program.change_experimental_features(Some(WarningLevel::Allow))
704}
705
706impl SketchApi for FrontendState {
707    async fn execute_mock(
708        &mut self,
709        ctx: &ExecutorContext,
710        _version: Version,
711        sketch: ObjectId,
712    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
713        let sketch_block_ref =
714            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
715
716        let mut truncated_program = self.program.clone();
717        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
718            .map_err(KclErrorWithOutputs::no_outputs)?;
719
720        // Execute.
721        let outcome = ctx
722            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
723            .await?;
724        let new_source = source_from_ast(&self.program.ast);
725        let src_delta = SourceDelta { text: new_source };
726        // MockConfig::default() has freedom_analysis: true
727        let outcome = self.update_state_after_exec(outcome, true);
728        let scene_graph_delta = SceneGraphDelta {
729            new_graph: self.scene_graph.clone(),
730            new_objects: Default::default(),
731            invalidates_ids: false,
732            exec_outcome: outcome,
733        };
734        Ok((src_delta, scene_graph_delta))
735    }
736
737    async fn new_sketch(
738        &mut self,
739        ctx: &ExecutorContext,
740        _project: ProjectId,
741        _file: FileId,
742        _version: Version,
743        args: SketchCtor,
744    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
745        // TODO: Check version.
746
747        let mut new_ast = self.program.ast.clone();
748        // Create updated KCL source from args.
749        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
750            .map_err(KclErrorWithOutputs::no_outputs)?;
751        let mut defined_names = find_defined_names(&new_ast);
752        let is_face_of_expr = matches!(
753            &plane_ast,
754            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
755        );
756        if is_face_of_expr {
757            let face_name = next_free_name_with_padding("face", &defined_names)
758                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
759            let face_decl = ast::VariableDeclaration::new(
760                ast::VariableDeclarator::new(&face_name, plane_ast),
761                ast::ItemVisibility::Default,
762                ast::VariableKind::Const,
763            );
764            new_ast
765                .body
766                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
767                    face_decl,
768                ))));
769            defined_names.insert(face_name.clone());
770            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
771        }
772        let sketch_ast = ast::SketchBlock {
773            arguments: vec![ast::LabeledArg {
774                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
775                arg: plane_ast,
776            }],
777            body: Default::default(),
778            is_being_edited: false,
779            non_code_meta: Default::default(),
780            digest: None,
781        };
782        // Add a sketch block as a variable declaration directly, avoiding
783        // source-range mutation on a no-src node.
784        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
785            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
786        let sketch_decl = ast::VariableDeclaration::new(
787            ast::VariableDeclarator::new(
788                &sketch_name,
789                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
790            ),
791            ast::ItemVisibility::Default,
792            ast::VariableKind::Const,
793        );
794        new_ast
795            .body
796            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
797                sketch_decl,
798            ))));
799        // Convert to string source to create real source ranges.
800        let new_source = source_from_ast(&new_ast);
801        // Parse the new source.
802        let new_program = parse_frontend_mutation_source(
803            &new_source,
804            "Error parsing KCL source after adding sketch",
805            "No AST produced after adding sketch",
806        )?;
807
808        // Make sure to only set this if there are no errors.
809        self.program = new_program.clone();
810
811        // We need to do an engine execute so that the plane object gets created
812        // and is cached.
813        let outcome = ctx.run_with_caching(new_program.clone()).await?;
814        let freedom_analysis_ran = true;
815
816        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
817
818        let Some(sketch_id) = self
819            .scene_graph
820            .objects
821            .iter()
822            .filter_map(|object| match object.kind {
823                ObjectKind::Sketch(_) => Some(object.id),
824                _ => None,
825            })
826            .max_by_key(|id| id.0)
827        else {
828            return Err(KclErrorWithOutputs::from_error_outcome(
829                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
830                outcome,
831            ));
832        };
833        // Store the object in the scene.
834        self.scene_graph.sketch_mode = Some(sketch_id);
835
836        let src_delta = SourceDelta { text: new_source };
837        let scene_graph_delta = SceneGraphDelta {
838            new_graph: self.scene_graph_for_ui(),
839            invalidates_ids: false,
840            new_objects: vec![sketch_id],
841            exec_outcome: outcome,
842        };
843        Ok((src_delta, scene_graph_delta, sketch_id))
844    }
845
846    async fn edit_sketch(
847        &mut self,
848        ctx: &ExecutorContext,
849        _project: ProjectId,
850        _file: FileId,
851        _version: Version,
852        sketch: ObjectId,
853    ) -> ExecResult<SceneGraphDelta> {
854        // TODO: Check version.
855
856        // Look up existing sketch.
857        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
858            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
859        })?;
860        let ObjectKind::Sketch(_) = &sketch_object.kind else {
861            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
862                "Object is not a sketch, it is {}",
863                sketch_object.kind.human_friendly_kind_with_article()
864            ))));
865        };
866        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
867
868        // Enter sketch mode by setting the sketch_mode.
869        self.scene_graph.sketch_mode = Some(sketch);
870
871        // Truncate after the sketch block for mock execution.
872        let mut truncated_program = self.program.clone();
873        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
874            .map_err(KclErrorWithOutputs::no_outputs)?;
875
876        // Execute in mock mode to ensure state is up to date. The caller will
877        // want freedom analysis to display segments correctly.
878        let outcome = ctx
879            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
880            .await?;
881
882        // MockConfig::default() has freedom_analysis: true
883        let outcome = self.update_state_after_exec(outcome, true);
884        let scene_graph_delta = SceneGraphDelta {
885            new_graph: self.scene_graph_for_ui(),
886            invalidates_ids: false,
887            new_objects: Vec::new(),
888            exec_outcome: outcome,
889        };
890        Ok(scene_graph_delta)
891    }
892
893    async fn exit_sketch(
894        &mut self,
895        ctx: &ExecutorContext,
896        _version: Version,
897        sketch: ObjectId,
898    ) -> ExecResult<SceneGraph> {
899        // TODO: Check version.
900        #[cfg(not(target_arch = "wasm32"))]
901        let _ = sketch;
902        #[cfg(target_arch = "wasm32")]
903        if self.scene_graph.sketch_mode != Some(sketch) {
904            web_sys::console::warn_1(
905                &format!(
906                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
907                    self.scene_graph.sketch_mode
908                )
909                .into(),
910            );
911        }
912        self.scene_graph.sketch_mode = None;
913
914        // Execute.
915        let outcome = ctx.run_with_caching(self.program.clone()).await?;
916
917        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
918        self.update_state_after_exec(outcome, false);
919
920        Ok(self.scene_graph_for_ui())
921    }
922
923    async fn delete_sketch(
924        &mut self,
925        ctx: &ExecutorContext,
926        _version: Version,
927        sketch: ObjectId,
928    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
929        // TODO: Check version.
930
931        let mut new_ast = self.program.ast.clone();
932
933        // Look up existing sketch.
934        let sketch_id = sketch;
935        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
936            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
937        })?;
938        let ObjectKind::Sketch(_) = &sketch_object.kind else {
939            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
940                "Object is not a sketch, it is {}",
941                sketch_object.kind.human_friendly_kind_with_article(),
942            ))));
943        };
944
945        // Modify the AST to remove the sketch.
946        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
947            .map_err(KclErrorWithOutputs::no_outputs)?;
948
949        self.execute_after_delete_sketch(ctx, &mut new_ast).await
950    }
951
952    async fn add_segment(
953        &mut self,
954        ctx: &ExecutorContext,
955        _version: Version,
956        sketch: ObjectId,
957        segment: SegmentCtor,
958        _label: Option<String>,
959    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
960        // TODO: Check version.
961        match segment {
962            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
963            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
964            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
965            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
966            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
967        }
968    }
969
970    async fn edit_segments(
971        &mut self,
972        ctx: &ExecutorContext,
973        _version: Version,
974        sketch: ObjectId,
975        segments: Vec<ExistingSegmentCtor>,
976    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
977        // TODO: Check version.
978        let sketch_block_ref =
979            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
980
981        let mut new_ast = self.program.ast.clone();
982        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
983        let mut invalidates_ids = false;
984
985        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
986        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
987        for segment in &segments {
988            edited_segment_ids.insert(segment.id);
989            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
990                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
991                && let ObjectKind::Segment {
992                    segment: Segment::ControlPointSpline(existing_spline),
993                } = &existing_object.kind
994                && existing_spline.controls.len() != new_ctor.points.len()
995            {
996                invalidates_ids = true;
997            }
998        }
999        let drag_anchor_segment_ids = self
1000            .next_drag_anchor_segment_ids
1001            .take()
1002            .unwrap_or_else(|| edited_segment_ids.clone());
1003        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
1004        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1005
1006        // Preprocess segments into a final_edits vector to handle if segments contains:
1007        // - edit start point of line1 (as SegmentCtor::Point)
1008        // - edit end point of line1 (as SegmentCtor::Point)
1009        //
1010        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1011        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1012        //
1013        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1014        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1015        // so the above example would result in a single line1 edit:
1016        // - the first start point edit creates a new line edit entry in final_edits
1017        // - the second end point edit finds this line edit and mutates the end position only.
1018        //
1019        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1020        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1021
1022        for segment in segments {
1023            let segment_id = segment.id;
1024            match segment.ctor {
1025                SegmentCtor::Point(ctor) => {
1026                    // Find the owner, if any (point -> line / arc)
1027                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1028                        && let ObjectKind::Segment { segment } = &segment_object.kind
1029                        && let Segment::Point(point) = segment
1030                        && let Some(owner_id) = point.owner
1031                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1032                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1033                    {
1034                        match owner_segment {
1035                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1036                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1037                                    let SegmentCtor::Line(line_ctor) = existing else {
1038                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1039                                            "Internal: Expected line ctor for owner, but found {}",
1040                                            existing.human_friendly_kind_with_article()
1041                                        ))));
1042                                    };
1043                                    // Line owner is already in final_edits -> apply this point edit
1044                                    if line.start == segment_id {
1045                                        line_ctor.start = ctor.position;
1046                                    } else {
1047                                        line_ctor.end = ctor.position;
1048                                    }
1049                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1050                                    // Line owner is not in final_edits yet -> create it
1051                                    let mut line_ctor = line_ctor.clone();
1052                                    if line.start == segment_id {
1053                                        line_ctor.start = ctor.position;
1054                                    } else {
1055                                        line_ctor.end = ctor.position;
1056                                    }
1057                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1058                                } else {
1059                                    // This should never run..
1060                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1061                                        "Internal: Line does not have line ctor, but found {}",
1062                                        line.ctor.human_friendly_kind_with_article()
1063                                    ))));
1064                                }
1065                                continue;
1066                            }
1067                            Segment::Arc(arc)
1068                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1069                            {
1070                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1071                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1072                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1073                                            "Internal: Expected arc ctor for owner, but found {}",
1074                                            existing.human_friendly_kind_with_article()
1075                                        ))));
1076                                    };
1077                                    if arc.start == segment_id {
1078                                        arc_ctor.start = ctor.position;
1079                                    } else if arc.end == segment_id {
1080                                        arc_ctor.end = ctor.position;
1081                                    } else {
1082                                        arc_ctor.center = ctor.position;
1083                                    }
1084                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1085                                    let mut arc_ctor = arc_ctor.clone();
1086                                    if arc.start == segment_id {
1087                                        arc_ctor.start = ctor.position;
1088                                    } else if arc.end == segment_id {
1089                                        arc_ctor.end = ctor.position;
1090                                    } else {
1091                                        arc_ctor.center = ctor.position;
1092                                    }
1093                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1094                                } else {
1095                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1096                                        "Internal: Arc does not have arc ctor, but found {}",
1097                                        arc.ctor.human_friendly_kind_with_article()
1098                                    ))));
1099                                }
1100                                continue;
1101                            }
1102                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1103                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1104                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1105                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1106                                            "Internal: Expected circle ctor for owner, but found {}",
1107                                            existing.human_friendly_kind_with_article()
1108                                        ))));
1109                                    };
1110                                    if circle.start == segment_id {
1111                                        circle_ctor.start = ctor.position;
1112                                    } else {
1113                                        circle_ctor.center = ctor.position;
1114                                    }
1115                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1116                                    let mut circle_ctor = circle_ctor.clone();
1117                                    if circle.start == segment_id {
1118                                        circle_ctor.start = ctor.position;
1119                                    } else {
1120                                        circle_ctor.center = ctor.position;
1121                                    }
1122                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1123                                } else {
1124                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125                                        "Internal: Circle does not have circle ctor, but found {}",
1126                                        circle.ctor.human_friendly_kind_with_article()
1127                                    ))));
1128                                }
1129                                continue;
1130                            }
1131                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1132                                let Some(control_index) =
1133                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1134                                else {
1135                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1136                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1137                                    ))));
1138                                };
1139                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1140                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1141                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1143                                            existing.human_friendly_kind_with_article()
1144                                        ))));
1145                                    };
1146                                    spline_ctor.points[control_index] = ctor.position;
1147                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1148                                    let mut spline_ctor = spline_ctor.clone();
1149                                    spline_ctor.points[control_index] = ctor.position;
1150                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1151                                } else {
1152                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1153                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1154                                        spline.ctor.human_friendly_kind_with_article()
1155                                    ))));
1156                                }
1157                                continue;
1158                            }
1159                            _ => {}
1160                        }
1161                    }
1162
1163                    // No owner, it's an individual point
1164                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1165                }
1166                SegmentCtor::Line(ctor) => {
1167                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1168                }
1169                SegmentCtor::Arc(ctor) => {
1170                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1171                }
1172                SegmentCtor::Circle(ctor) => {
1173                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1174                }
1175                SegmentCtor::ControlPointSpline(ctor) => {
1176                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1177                }
1178            }
1179        }
1180
1181        for (segment_id, ctor) in final_edits {
1182            match ctor {
1183                SegmentCtor::Point(ctor) => self
1184                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1185                    .map_err(KclErrorWithOutputs::no_outputs)?,
1186                SegmentCtor::Line(ctor) => self
1187                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1188                    .map_err(KclErrorWithOutputs::no_outputs)?,
1189                SegmentCtor::Arc(ctor) => self
1190                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1191                    .map_err(KclErrorWithOutputs::no_outputs)?,
1192                SegmentCtor::Circle(ctor) => self
1193                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1194                    .map_err(KclErrorWithOutputs::no_outputs)?,
1195                SegmentCtor::ControlPointSpline(ctor) => self
1196                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1197                    .map_err(KclErrorWithOutputs::no_outputs)?,
1198            }
1199        }
1200        for edit in constraint_label_edits {
1201            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1202                .map_err(KclErrorWithOutputs::no_outputs)?;
1203        }
1204        let (source_delta, mut scene_graph_delta) = self
1205            .execute_after_edit(
1206                ctx,
1207                sketch,
1208                sketch_block_ref,
1209                &mut new_ast,
1210                ExecuteAfterEditOptions {
1211                    segment_ids_edited: drag_anchor_segment_ids,
1212                    edit_kind: EditDeleteKind::Edit,
1213                    commit_solved_initial_guesses,
1214                },
1215            )
1216            .await?;
1217        if invalidates_ids {
1218            scene_graph_delta.invalidates_ids = true;
1219        }
1220        Ok((source_delta, scene_graph_delta))
1221    }
1222
1223    async fn delete_objects(
1224        &mut self,
1225        ctx: &ExecutorContext,
1226        _version: Version,
1227        sketch: ObjectId,
1228        constraint_ids: Vec<ObjectId>,
1229        segment_ids: Vec<ObjectId>,
1230    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1231        // TODO: Check version.
1232        let sketch_block_ref =
1233            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1234
1235        // Deduplicate IDs.
1236        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1237        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1238
1239        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1240        // also delete the point, as well as other points that are owned by the owner.
1241        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1242
1243        for segment_id in segment_ids_set.iter().copied() {
1244            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1245                let ObjectKind::Segment { segment } = &segment_object.kind else {
1246                    return None;
1247                };
1248                match segment {
1249                    Segment::Point(point) => point.owner,
1250                    Segment::Line(line) => line.owner,
1251                    _ => None,
1252                }
1253            });
1254
1255            if let Some(owner_id) = owner_id
1256                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1257                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1258                && matches!(
1259                    owner_segment,
1260                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1261                )
1262            {
1263                // segment is owned -> delete the owner
1264                resolved_segment_ids_to_delete.insert(owner_id);
1265            } else {
1266                // segment is not owned by anything -> can be deleted
1267                resolved_segment_ids_to_delete.insert(segment_id);
1268            }
1269        }
1270        let referenced_constraint_ids = self
1271            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1272            .map_err(KclErrorWithOutputs::no_outputs)?;
1273
1274        let mut new_ast = self.program.ast.clone();
1275
1276        for constraint_id in referenced_constraint_ids {
1277            if constraint_ids_set.contains(&constraint_id) {
1278                continue;
1279            }
1280
1281            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1282                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1283            })?;
1284            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1285                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1286                    "Object is not a constraint, it is {}",
1287                    constraint_object.kind.human_friendly_kind_with_article()
1288                ))));
1289            };
1290
1291            match constraint {
1292                Constraint::Coincident(coincident) => {
1293                    let remaining_segments =
1294                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1295
1296                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1297                    if remaining_segments.len() >= 2 {
1298                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1299                            .map_err(KclErrorWithOutputs::no_outputs)?;
1300                    } else {
1301                        constraint_ids_set.insert(constraint_id);
1302                    }
1303                }
1304                Constraint::EqualRadius(equal_radius) => {
1305                    let remaining_input = equal_radius
1306                        .input
1307                        .iter()
1308                        .copied()
1309                        .filter(|segment_id| {
1310                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1311                        })
1312                        .collect::<Vec<_>>();
1313
1314                    if remaining_input.len() >= 2 {
1315                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1316                            .map_err(KclErrorWithOutputs::no_outputs)?;
1317                    } else {
1318                        constraint_ids_set.insert(constraint_id);
1319                    }
1320                }
1321                Constraint::LinesEqualLength(lines_equal_length) => {
1322                    let remaining_lines = lines_equal_length
1323                        .lines
1324                        .iter()
1325                        .copied()
1326                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1327                        .collect::<Vec<_>>();
1328
1329                    // Equal length constraint is only valid with at least 2 lines
1330                    if remaining_lines.len() >= 2 {
1331                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1332                            .map_err(KclErrorWithOutputs::no_outputs)?;
1333                    } else {
1334                        constraint_ids_set.insert(constraint_id);
1335                    }
1336                }
1337                Constraint::Parallel(parallel) => {
1338                    let remaining_lines = parallel
1339                        .lines
1340                        .iter()
1341                        .copied()
1342                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1343                        .collect::<Vec<_>>();
1344
1345                    if remaining_lines.len() >= 2 {
1346                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1347                            .map_err(KclErrorWithOutputs::no_outputs)?;
1348                    } else {
1349                        constraint_ids_set.insert(constraint_id);
1350                    }
1351                }
1352                Constraint::Horizontal(Horizontal::Points { points }) => {
1353                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1354
1355                    if remaining_points.len() >= 2 {
1356                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1357                            .map_err(KclErrorWithOutputs::no_outputs)?;
1358                    } else {
1359                        constraint_ids_set.insert(constraint_id);
1360                    }
1361                }
1362                Constraint::Vertical(Vertical::Points { points }) => {
1363                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1364
1365                    if remaining_points.len() >= 2 {
1366                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1367                            .map_err(KclErrorWithOutputs::no_outputs)?;
1368                    } else {
1369                        constraint_ids_set.insert(constraint_id);
1370                    }
1371                }
1372                Constraint::Fixed(fixed) => {
1373                    if fixed.points.iter().any(|fixed_point| {
1374                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1375                    }) {
1376                        constraint_ids_set.insert(constraint_id);
1377                    }
1378                }
1379                _ => {
1380                    // All other constraint types: if referenced by a segment -> delete the constraint
1381                    constraint_ids_set.insert(constraint_id);
1382                }
1383            }
1384        }
1385
1386        for constraint_id in constraint_ids_set {
1387            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1388                .map_err(KclErrorWithOutputs::no_outputs)?;
1389        }
1390        for segment_id in resolved_segment_ids_to_delete {
1391            self.delete_segment(&mut new_ast, sketch, segment_id)
1392                .map_err(KclErrorWithOutputs::no_outputs)?;
1393        }
1394
1395        self.execute_after_edit(
1396            ctx,
1397            sketch,
1398            sketch_block_ref,
1399            &mut new_ast,
1400            ExecuteAfterEditOptions {
1401                segment_ids_edited: Default::default(),
1402                edit_kind: EditDeleteKind::DeleteNonSketch,
1403                commit_solved_initial_guesses: true,
1404            },
1405        )
1406        .await
1407    }
1408
1409    async fn add_constraint(
1410        &mut self,
1411        ctx: &ExecutorContext,
1412        _version: Version,
1413        sketch: ObjectId,
1414        constraint: Constraint,
1415    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1416        // TODO: Check version.
1417
1418        // Save the original state as a backup - we'll restore it if anything fails
1419        let original_program = self.program.clone();
1420        let original_scene_graph = self.scene_graph.clone();
1421
1422        let mut new_ast = self.program.ast.clone();
1423        let sketch_block_ref = match constraint {
1424            Constraint::Coincident(coincident) => self
1425                .add_coincident(sketch, coincident, &mut new_ast)
1426                .await
1427                .map_err(KclErrorWithOutputs::no_outputs)?,
1428            Constraint::Distance(distance) => self
1429                .add_distance(sketch, distance, &mut new_ast)
1430                .await
1431                .map_err(KclErrorWithOutputs::no_outputs)?,
1432            Constraint::EqualRadius(equal_radius) => self
1433                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1434                .await
1435                .map_err(KclErrorWithOutputs::no_outputs)?,
1436            Constraint::Fixed(fixed) => self
1437                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1438                .await
1439                .map_err(KclErrorWithOutputs::no_outputs)?,
1440            Constraint::HorizontalDistance(distance) => self
1441                .add_horizontal_distance(sketch, distance, &mut new_ast)
1442                .await
1443                .map_err(KclErrorWithOutputs::no_outputs)?,
1444            Constraint::VerticalDistance(distance) => self
1445                .add_vertical_distance(sketch, distance, &mut new_ast)
1446                .await
1447                .map_err(KclErrorWithOutputs::no_outputs)?,
1448            Constraint::Horizontal(horizontal) => self
1449                .add_horizontal(sketch, horizontal, &mut new_ast)
1450                .await
1451                .map_err(KclErrorWithOutputs::no_outputs)?,
1452            Constraint::LinesEqualLength(lines_equal_length) => self
1453                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1454                .await
1455                .map_err(KclErrorWithOutputs::no_outputs)?,
1456            Constraint::Midpoint(midpoint) => self
1457                .add_midpoint(sketch, midpoint, &mut new_ast)
1458                .await
1459                .map_err(KclErrorWithOutputs::no_outputs)?,
1460            Constraint::Parallel(parallel) => self
1461                .add_parallel(sketch, parallel, &mut new_ast)
1462                .await
1463                .map_err(KclErrorWithOutputs::no_outputs)?,
1464            Constraint::Perpendicular(perpendicular) => self
1465                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1466                .await
1467                .map_err(KclErrorWithOutputs::no_outputs)?,
1468            Constraint::Radius(radius) => self
1469                .add_radius(sketch, radius, &mut new_ast)
1470                .await
1471                .map_err(KclErrorWithOutputs::no_outputs)?,
1472            Constraint::Diameter(diameter) => self
1473                .add_diameter(sketch, diameter, &mut new_ast)
1474                .await
1475                .map_err(KclErrorWithOutputs::no_outputs)?,
1476            Constraint::Symmetric(symmetric) => self
1477                .add_symmetric(sketch, symmetric, &mut new_ast)
1478                .await
1479                .map_err(KclErrorWithOutputs::no_outputs)?,
1480            Constraint::Vertical(vertical) => self
1481                .add_vertical(sketch, vertical, &mut new_ast)
1482                .await
1483                .map_err(KclErrorWithOutputs::no_outputs)?,
1484            Constraint::Angle(lines_at_angle) => self
1485                .add_angle(sketch, lines_at_angle, &mut new_ast)
1486                .await
1487                .map_err(KclErrorWithOutputs::no_outputs)?,
1488            Constraint::Tangent(tangent) => self
1489                .add_tangent(sketch, tangent, &mut new_ast)
1490                .await
1491                .map_err(KclErrorWithOutputs::no_outputs)?,
1492        };
1493
1494        let result = self
1495            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1496            .await;
1497
1498        // If execution failed, restore the original state to prevent corruption
1499        if result.is_err() {
1500            self.program = original_program;
1501            self.scene_graph = original_scene_graph;
1502        }
1503
1504        result
1505    }
1506
1507    async fn chain_segment(
1508        &mut self,
1509        ctx: &ExecutorContext,
1510        version: Version,
1511        sketch: ObjectId,
1512        previous_segment_end_point_id: ObjectId,
1513        segment: SegmentCtor,
1514        _label: Option<String>,
1515    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1516        // TODO: Check version.
1517
1518        // First, add the segment (line) to get its start point ID
1519        let SegmentCtor::Line(line_ctor) = segment else {
1520            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1521                "chain_segment currently only supports Line segments, got {}",
1522                segment.human_friendly_kind_with_article(),
1523            ))));
1524        };
1525
1526        // Add the line segment first - this updates self.program and self.scene_graph
1527        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1528
1529        // Find the new line's start point ID from the updated scene graph
1530        // add_line updates self.scene_graph, so we can use that
1531        let new_line_id = first_scene_delta
1532            .new_objects
1533            .iter()
1534            .find(|&obj_id| {
1535                let obj = self.scene_graph.objects.get(obj_id.0);
1536                if let Some(obj) = obj {
1537                    matches!(
1538                        &obj.kind,
1539                        ObjectKind::Segment {
1540                            segment: Segment::Line(_)
1541                        }
1542                    )
1543                } else {
1544                    false
1545                }
1546            })
1547            .ok_or_else(|| {
1548                KclErrorWithOutputs::no_outputs(KclError::refactor(
1549                    "Failed to find new line segment in scene graph".to_string(),
1550                ))
1551            })?;
1552
1553        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1554            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1555                "New line object not found: {new_line_id:?}"
1556            )))
1557        })?;
1558
1559        let ObjectKind::Segment {
1560            segment: new_line_segment,
1561        } = &new_line_obj.kind
1562        else {
1563            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1564                "Object is not a segment: {new_line_obj:?}"
1565            ))));
1566        };
1567
1568        let Segment::Line(new_line) = new_line_segment else {
1569            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1570                "Segment is not a line: {new_line_segment:?}"
1571            ))));
1572        };
1573
1574        let new_line_start_point_id = new_line.start;
1575
1576        // Now add the coincident constraint between the previous end point and the new line's start point.
1577        let coincident = Coincident {
1578            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1579        };
1580
1581        let (final_src_delta, final_scene_delta) = self
1582            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1583            .await?;
1584
1585        // Combine new objects from the line addition and the constraint addition.
1586        // Both add_line and add_constraint now populate new_objects correctly.
1587        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1588        combined_new_objects.extend(final_scene_delta.new_objects);
1589
1590        let scene_graph_delta = SceneGraphDelta {
1591            new_graph: self.scene_graph_for_ui(),
1592            invalidates_ids: false,
1593            new_objects: combined_new_objects,
1594            exec_outcome: final_scene_delta.exec_outcome,
1595        };
1596
1597        Ok((final_src_delta, scene_graph_delta))
1598    }
1599
1600    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1601    async fn edit_constraint_value(
1602        &mut self,
1603        ctx: &ExecutorContext,
1604        _version: Version,
1605        sketch: ObjectId,
1606        constraint_id: ObjectId,
1607        value_expression: String,
1608    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1609        // TODO: Check version.
1610        let sketch_block_ref =
1611            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1612
1613        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1614            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1615        })?;
1616        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1617            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1618                "Object is not a constraint: {constraint_id:?}"
1619            ))));
1620        }
1621
1622        let mut new_ast = self.program.ast.clone();
1623
1624        // Parse the expression string into an AST node.
1625        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1626            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1627                "Invalid constraint value",
1628                &e,
1629            )))
1630        })?;
1631        if !errors.is_empty() {
1632            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1633                format_compilation_issues("Invalid constraint value", &errors),
1634            )));
1635        }
1636        let mut parsed = parsed.ok_or_else(|| {
1637            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1638        })?;
1639        if parsed.ast.body.is_empty() {
1640            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1641                "Empty value expression".to_string(),
1642            )));
1643        }
1644        let first = parsed.ast.body.remove(0);
1645        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1646            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1647                "Value expression must be a simple expression".to_string(),
1648            )));
1649        };
1650
1651        let new_value: ast::BinaryPart = expr_stmt
1652            .inner
1653            .expression
1654            .try_into()
1655            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1656
1657        self.mutate_ast(
1658            &mut new_ast,
1659            constraint_id,
1660            AstMutateCommand::EditConstraintValue { value: new_value },
1661        )
1662        .map_err(KclErrorWithOutputs::no_outputs)?;
1663
1664        self.execute_after_edit(
1665            ctx,
1666            sketch,
1667            sketch_block_ref,
1668            &mut new_ast,
1669            ExecuteAfterEditOptions {
1670                segment_ids_edited: Default::default(),
1671                edit_kind: EditDeleteKind::Edit,
1672                commit_solved_initial_guesses: true,
1673            },
1674        )
1675        .await
1676    }
1677
1678    async fn edit_distance_constraint_label_position(
1679        &mut self,
1680        ctx: &ExecutorContext,
1681        _version: Version,
1682        sketch: ObjectId,
1683        constraint_id: ObjectId,
1684        label_position: Point2d<Number>,
1685        anchor_segment_ids: Vec<ObjectId>,
1686    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1687        // TODO: Check version.
1688        let sketch_block_ref =
1689            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1690
1691        let mut new_ast = self.program.ast.clone();
1692        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1693            .map_err(KclErrorWithOutputs::no_outputs)?;
1694        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1695
1696        self.execute_after_edit(
1697            ctx,
1698            sketch,
1699            sketch_block_ref,
1700            &mut new_ast,
1701            ExecuteAfterEditOptions {
1702                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1703                edit_kind: EditDeleteKind::Edit,
1704                commit_solved_initial_guesses,
1705            },
1706        )
1707        .await
1708    }
1709
1710    /// Splitting a segment means creating a new segment, editing the old one, and then
1711    /// migrating a bunch of the constraints from the original segment to the new one
1712    /// (i.e. deleting them and re-adding them on the other segment).
1713    ///
1714    /// To keep this efficient we require as few executions as possible: we create the
1715    /// new segment first (to get its id), then do all edits and new constraints, and
1716    /// do all deletes at the end (since deletes invalidate ids).
1717    async fn batch_split_segment_operations(
1718        &mut self,
1719        ctx: &ExecutorContext,
1720        _version: Version,
1721        sketch: ObjectId,
1722        edit_segments: Vec<ExistingSegmentCtor>,
1723        add_constraints: Vec<Constraint>,
1724        delete_constraint_ids: Vec<ObjectId>,
1725        _new_segment_info: sketch::NewSegmentInfo,
1726    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1727        // TODO: Check version.
1728        let sketch_block_ref =
1729            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1730
1731        let mut new_ast = self.program.ast.clone();
1732        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1733
1734        // Step 1: Edit segments
1735        for segment in edit_segments {
1736            segment_ids_edited.insert(segment.id);
1737            match segment.ctor {
1738                SegmentCtor::Point(ctor) => self
1739                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1740                    .map_err(KclErrorWithOutputs::no_outputs)?,
1741                SegmentCtor::Line(ctor) => self
1742                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1743                    .map_err(KclErrorWithOutputs::no_outputs)?,
1744                SegmentCtor::Arc(ctor) => self
1745                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1746                    .map_err(KclErrorWithOutputs::no_outputs)?,
1747                SegmentCtor::Circle(ctor) => self
1748                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1749                    .map_err(KclErrorWithOutputs::no_outputs)?,
1750                SegmentCtor::ControlPointSpline(ctor) => self
1751                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1752                    .map_err(KclErrorWithOutputs::no_outputs)?,
1753            }
1754        }
1755
1756        // Step 2: Add all constraints
1757        for constraint in add_constraints {
1758            match constraint {
1759                Constraint::Coincident(coincident) => {
1760                    self.add_coincident(sketch, coincident, &mut new_ast)
1761                        .await
1762                        .map_err(KclErrorWithOutputs::no_outputs)?;
1763                }
1764                Constraint::Distance(distance) => {
1765                    self.add_distance(sketch, distance, &mut new_ast)
1766                        .await
1767                        .map_err(KclErrorWithOutputs::no_outputs)?;
1768                }
1769                Constraint::EqualRadius(equal_radius) => {
1770                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1771                        .await
1772                        .map_err(KclErrorWithOutputs::no_outputs)?;
1773                }
1774                Constraint::Fixed(fixed) => {
1775                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1776                        .await
1777                        .map_err(KclErrorWithOutputs::no_outputs)?;
1778                }
1779                Constraint::HorizontalDistance(distance) => {
1780                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1781                        .await
1782                        .map_err(KclErrorWithOutputs::no_outputs)?;
1783                }
1784                Constraint::VerticalDistance(distance) => {
1785                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1786                        .await
1787                        .map_err(KclErrorWithOutputs::no_outputs)?;
1788                }
1789                Constraint::Horizontal(horizontal) => {
1790                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1791                        .await
1792                        .map_err(KclErrorWithOutputs::no_outputs)?;
1793                }
1794                Constraint::LinesEqualLength(lines_equal_length) => {
1795                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1796                        .await
1797                        .map_err(KclErrorWithOutputs::no_outputs)?;
1798                }
1799                Constraint::Midpoint(midpoint) => {
1800                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1801                        .await
1802                        .map_err(KclErrorWithOutputs::no_outputs)?;
1803                }
1804                Constraint::Parallel(parallel) => {
1805                    self.add_parallel(sketch, parallel, &mut new_ast)
1806                        .await
1807                        .map_err(KclErrorWithOutputs::no_outputs)?;
1808                }
1809                Constraint::Perpendicular(perpendicular) => {
1810                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1811                        .await
1812                        .map_err(KclErrorWithOutputs::no_outputs)?;
1813                }
1814                Constraint::Vertical(vertical) => {
1815                    self.add_vertical(sketch, vertical, &mut new_ast)
1816                        .await
1817                        .map_err(KclErrorWithOutputs::no_outputs)?;
1818                }
1819                Constraint::Diameter(diameter) => {
1820                    self.add_diameter(sketch, diameter, &mut new_ast)
1821                        .await
1822                        .map_err(KclErrorWithOutputs::no_outputs)?;
1823                }
1824                Constraint::Radius(radius) => {
1825                    self.add_radius(sketch, radius, &mut new_ast)
1826                        .await
1827                        .map_err(KclErrorWithOutputs::no_outputs)?;
1828                }
1829                Constraint::Symmetric(symmetric) => {
1830                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1831                        .await
1832                        .map_err(KclErrorWithOutputs::no_outputs)?;
1833                }
1834                Constraint::Angle(angle) => {
1835                    self.add_angle(sketch, angle, &mut new_ast)
1836                        .await
1837                        .map_err(KclErrorWithOutputs::no_outputs)?;
1838                }
1839                Constraint::Tangent(tangent) => {
1840                    self.add_tangent(sketch, tangent, &mut new_ast)
1841                        .await
1842                        .map_err(KclErrorWithOutputs::no_outputs)?;
1843                }
1844            }
1845        }
1846
1847        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1848        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1849
1850        let has_constraint_deletions = !constraint_ids_set.is_empty();
1851        for constraint_id in constraint_ids_set {
1852            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1853                .map_err(KclErrorWithOutputs::no_outputs)?;
1854        }
1855
1856        // Step 4: Execute once at the end
1857        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1858        // But we'll manually set invalidates_ids: true if we deleted constraints
1859        let (source_delta, mut scene_graph_delta) = self
1860            .execute_after_edit(
1861                ctx,
1862                sketch,
1863                sketch_block_ref,
1864                &mut new_ast,
1865                ExecuteAfterEditOptions {
1866                    segment_ids_edited,
1867                    edit_kind: EditDeleteKind::Edit,
1868                    commit_solved_initial_guesses: true,
1869                },
1870            )
1871            .await?;
1872
1873        // If we deleted constraints, set invalidates_ids: true
1874        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1875        if has_constraint_deletions {
1876            scene_graph_delta.invalidates_ids = true;
1877        }
1878
1879        Ok((source_delta, scene_graph_delta))
1880    }
1881
1882    async fn batch_tail_cut_operations(
1883        &mut self,
1884        ctx: &ExecutorContext,
1885        _version: Version,
1886        sketch: ObjectId,
1887        edit_segments: Vec<ExistingSegmentCtor>,
1888        add_constraints: Vec<Constraint>,
1889        delete_constraint_ids: Vec<ObjectId>,
1890        additional_edited_segment_ids: Vec<ObjectId>,
1891    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1892        let sketch_block_ref =
1893            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1894
1895        let mut new_ast = self.program.ast.clone();
1896        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1897
1898        // Step 1: Edit segments (usually a single segment for tail cut)
1899        for segment in edit_segments {
1900            segment_ids_edited.insert(segment.id);
1901            match segment.ctor {
1902                SegmentCtor::Point(ctor) => self
1903                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1904                    .map_err(KclErrorWithOutputs::no_outputs)?,
1905                SegmentCtor::Line(ctor) => self
1906                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1907                    .map_err(KclErrorWithOutputs::no_outputs)?,
1908                SegmentCtor::Arc(ctor) => self
1909                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1910                    .map_err(KclErrorWithOutputs::no_outputs)?,
1911                SegmentCtor::Circle(ctor) => self
1912                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1913                    .map_err(KclErrorWithOutputs::no_outputs)?,
1914                SegmentCtor::ControlPointSpline(ctor) => self
1915                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1916                    .map_err(KclErrorWithOutputs::no_outputs)?,
1917            }
1918        }
1919
1920        segment_ids_edited.extend(additional_edited_segment_ids);
1921
1922        // Step 2: Add coincident constraints
1923        for constraint in add_constraints {
1924            match constraint {
1925                Constraint::Coincident(coincident) => {
1926                    self.add_coincident(sketch, coincident, &mut new_ast)
1927                        .await
1928                        .map_err(KclErrorWithOutputs::no_outputs)?;
1929                }
1930                other => {
1931                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1932                        "unsupported constraint in tail cut batch: {other:?}"
1933                    ))));
1934                }
1935            }
1936        }
1937
1938        // Step 3: Delete constraints (if any)
1939        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1940
1941        let has_constraint_deletions = !constraint_ids_set.is_empty();
1942        for constraint_id in constraint_ids_set {
1943            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1944                .map_err(KclErrorWithOutputs::no_outputs)?;
1945        }
1946
1947        // Step 4: Single execute_after_edit
1948        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1949        // But we'll manually set invalidates_ids: true if we deleted constraints
1950        let (source_delta, mut scene_graph_delta) = self
1951            .execute_after_edit(
1952                ctx,
1953                sketch,
1954                sketch_block_ref,
1955                &mut new_ast,
1956                ExecuteAfterEditOptions {
1957                    segment_ids_edited,
1958                    edit_kind: EditDeleteKind::Edit,
1959                    commit_solved_initial_guesses: true,
1960                },
1961            )
1962            .await?;
1963
1964        // If we deleted constraints, set invalidates_ids: true
1965        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1966        if has_constraint_deletions {
1967            scene_graph_delta.invalidates_ids = true;
1968        }
1969
1970        Ok((source_delta, scene_graph_delta))
1971    }
1972}
1973
1974impl FrontendState {
1975    /// Evaluate an input expression using the current model's settings and cached variables.
1976    pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1977        program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1978        ctx.run_mock(&program, &MockConfig::default()).await
1979    }
1980
1981    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1982        self.program = program.clone();
1983
1984        // Execute so that the objects are updated and available for the next
1985        // API call.
1986        // This always uses engine execution (not mock) so that things are cached.
1987        // Engine execution now runs freedom analysis automatically.
1988        // Keep existing checkpoints alive here. History may still reference
1989        // older committed sketch states across a direct-edit boundary, and a
1990        // checkpoint restore is a full state replacement anyway. We append a
1991        // fresh baseline checkpoint after the full execution below.
1992        // Clear the freedom cache since IDs might have changed after direct editing
1993        // and we're about to run freedom analysis which will repopulate it.
1994        self.point_freedom_cache.clear();
1995        match ctx.run_with_caching(program).await {
1996            Ok(outcome) => {
1997                let outcome = self.update_state_after_exec(outcome, true);
1998                let checkpoint_id = self
1999                    .create_sketch_checkpoint(&ctx.cache, outcome.clone())
2000                    .await
2001                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
2002                Ok(SetProgramOutcome::Success {
2003                    scene_graph: Box::new(self.scene_graph_for_ui()),
2004                    exec_outcome: Box::new(outcome),
2005                    checkpoint_id: Some(checkpoint_id),
2006                })
2007            }
2008            Err(mut err) => {
2009                // Don't return an error just because execution failed. Instead,
2010                // update state as much as possible.
2011                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2012                self.update_state_after_exec(outcome, true);
2013                err.scene_graph = Some(self.scene_graph_for_ui());
2014                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2015            }
2016        }
2017    }
2018
2019    /// Decorate engine execution such that our state is updated and the scene
2020    /// graph is added to the return.
2021    pub async fn engine_execute(
2022        &mut self,
2023        ctx: &ExecutorContext,
2024        program: Program,
2025    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2026        self.program = program.clone();
2027
2028        // Engine execution now runs freedom analysis automatically. Clear the
2029        // freedom cache since IDs might have changed after direct editing, and
2030        // we're about to run freedom analysis which will repopulate it.
2031        self.point_freedom_cache.clear();
2032        match ctx.run_with_caching(program).await {
2033            Ok(outcome) => {
2034                let outcome = self.update_state_after_exec(outcome, true);
2035                Ok(SceneGraphDelta {
2036                    new_graph: self.scene_graph_for_ui(),
2037                    exec_outcome: outcome,
2038                    // We don't know what the new objects are.
2039                    new_objects: Default::default(),
2040                    // We don't know if IDs were invalidated.
2041                    invalidates_ids: Default::default(),
2042                })
2043            }
2044            Err(mut err) => {
2045                // Update state as much as possible, even when there's an error.
2046                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2047                self.update_state_after_exec(outcome, true);
2048                err.scene_graph = Some(self.scene_graph_for_ui());
2049                Err(err)
2050            }
2051        }
2052    }
2053
2054    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2055        if matches!(err.error, KclError::EngineHangup { .. }) {
2056            // It's not ideal to special-case this, but this error is very
2057            // common during development, and it causes confusing downstream
2058            // errors that have nothing to do with the actual problem.
2059            return Err(err);
2060        }
2061
2062        let KclErrorWithOutputs {
2063            error,
2064            mut non_fatal,
2065            variables,
2066            operations,
2067            artifact_graph,
2068            scene_objects,
2069            source_range_to_object,
2070            var_solutions,
2071            refactor_metadata,
2072            filenames,
2073            source_files,
2074            default_planes,
2075            engine_scene_generation,
2076            ..
2077        } = err;
2078
2079        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2080
2081        Ok(ExecOutcome {
2082            variables,
2083            #[cfg(test)]
2084            test_program_memory: Default::default(),
2085            filenames,
2086            // Errors from run_with_caching always carry the generation, and
2087            // update_state_after_exec doesn't read it anyway. TODO: return a
2088            // narrower type than ExecOutcome so this doesn't need to be filled in?
2089            engine_scene_generation: engine_scene_generation.unwrap_or_default(),
2090            operations,
2091            artifact_graph,
2092            scene_objects,
2093            source_range_to_object,
2094            var_solutions,
2095            refactor_metadata,
2096            issues: non_fatal,
2097            source_files,
2098            default_planes,
2099        })
2100    }
2101
2102    async fn add_point(
2103        &mut self,
2104        ctx: &ExecutorContext,
2105        sketch: ObjectId,
2106        ctor: PointCtor,
2107    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2108        // Create updated KCL source from args.
2109        let at_ast = to_ast_point2d(&ctor.position)
2110            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2111        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2112            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2113            unlabeled: None,
2114            arguments: vec![ast::LabeledArg {
2115                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2116                arg: at_ast,
2117            }],
2118            digest: None,
2119            non_code_meta: Default::default(),
2120        })));
2121
2122        // Look up existing sketch.
2123        let sketch_id = sketch;
2124        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2125            #[cfg(target_arch = "wasm32")]
2126            web_sys::console::error_1(
2127                &format!(
2128                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2129                    self.scene_graph.objects
2130                )
2131                .into(),
2132            );
2133            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2134        })?;
2135        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2136            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137                "Object is not a sketch, it is {}",
2138                sketch_object.kind.human_friendly_kind_with_article(),
2139            ))));
2140        };
2141        // Add the point to the AST of the sketch block.
2142        let mut new_ast = self.program.ast.clone();
2143        let (sketch_block_ref, _) = self
2144            .mutate_ast(
2145                &mut new_ast,
2146                sketch_id,
2147                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2148            )
2149            .map_err(KclErrorWithOutputs::no_outputs)?;
2150        // Convert to string source to create real source ranges.
2151        let new_source = source_from_ast(&new_ast);
2152        // Parse the new KCL source.
2153        let new_program = parse_frontend_mutation_source(
2154            &new_source,
2155            "Error parsing KCL source after adding point",
2156            "No AST produced after adding point",
2157        )?;
2158
2159        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2160            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2161                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2162            )))
2163        })?;
2164
2165        // Make sure to only set this if there are no errors.
2166        self.program = new_program.clone();
2167
2168        // Truncate after the sketch block for mock execution.
2169        let mut truncated_program = new_program;
2170        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2171            .map_err(KclErrorWithOutputs::no_outputs)?;
2172
2173        // Execute.
2174        let outcome = ctx
2175            .run_mock(
2176                &truncated_program,
2177                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2178            )
2179            .await?;
2180
2181        let new_object_ids = {
2182            let make_err =
2183                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2184            let segment_id = outcome
2185                .source_range_to_object
2186                .get(&point_node_ref.range)
2187                .copied()
2188                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2189            let segment_object = outcome
2190                .scene_objects
2191                .get(segment_id.0)
2192                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2193            let ObjectKind::Segment { segment } = &segment_object.kind else {
2194                return Err(make_err(format!(
2195                    "Object is not a segment, it is {}",
2196                    segment_object.kind.human_friendly_kind_with_article()
2197                )));
2198            };
2199            let Segment::Point(_) = segment else {
2200                return Err(make_err(format!(
2201                    "Segment is not a point, it is {}",
2202                    segment.human_friendly_kind_with_article()
2203                )));
2204            };
2205            vec![segment_id]
2206        };
2207        let src_delta = SourceDelta { text: new_source };
2208        // Uses .no_freedom_analysis() so freedom_analysis: false
2209        let outcome = self.update_state_after_exec(outcome, false);
2210        let scene_graph_delta = SceneGraphDelta {
2211            new_graph: self.scene_graph_for_ui(),
2212            invalidates_ids: false,
2213            new_objects: new_object_ids,
2214            exec_outcome: outcome,
2215        };
2216        Ok((src_delta, scene_graph_delta))
2217    }
2218
2219    async fn add_line(
2220        &mut self,
2221        ctx: &ExecutorContext,
2222        sketch: ObjectId,
2223        ctor: LineCtor,
2224    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2225        // Create updated KCL source from args.
2226        let start_ast = to_ast_point2d(&ctor.start)
2227            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2228        let end_ast = to_ast_point2d(&ctor.end)
2229            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2230        let mut arguments = vec![
2231            ast::LabeledArg {
2232                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2233                arg: start_ast,
2234            },
2235            ast::LabeledArg {
2236                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2237                arg: end_ast,
2238            },
2239        ];
2240        // Add construction kwarg if construction is Some(true)
2241        if ctor.construction == Some(true) {
2242            arguments.push(ast::LabeledArg {
2243                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2244                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2245                    value: ast::LiteralValue::Bool(true),
2246                    raw: "true".to_string(),
2247                    digest: None,
2248                }))),
2249            });
2250        }
2251        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2252            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2253            unlabeled: None,
2254            arguments,
2255            digest: None,
2256            non_code_meta: Default::default(),
2257        })));
2258
2259        // Look up existing sketch.
2260        let sketch_id = sketch;
2261        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2262            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2263        })?;
2264        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2265            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266                "Object is not a sketch, it is {}",
2267                sketch_object.kind.human_friendly_kind_with_article(),
2268            ))));
2269        };
2270        // Add the line to the AST of the sketch block.
2271        let mut new_ast = self.program.ast.clone();
2272        let (sketch_block_ref, _) = self
2273            .mutate_ast(
2274                &mut new_ast,
2275                sketch_id,
2276                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2277            )
2278            .map_err(KclErrorWithOutputs::no_outputs)?;
2279        // Convert to string source to create real source ranges.
2280        let new_source = source_from_ast(&new_ast);
2281        // Parse the new KCL source.
2282        let new_program = parse_frontend_mutation_source(
2283            &new_source,
2284            "Error parsing KCL source after adding line",
2285            "No AST produced after adding line",
2286        )?;
2287
2288        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2289            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2290                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2291            )))
2292        })?;
2293
2294        // Make sure to only set this if there are no errors.
2295        self.program = new_program.clone();
2296
2297        // Truncate after the sketch block for mock execution.
2298        let mut truncated_program = new_program;
2299        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2300            .map_err(KclErrorWithOutputs::no_outputs)?;
2301
2302        // Execute.
2303        let outcome = ctx
2304            .run_mock(
2305                &truncated_program,
2306                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2307            )
2308            .await?;
2309
2310        let new_object_ids = {
2311            let make_err =
2312                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2313            let segment_id = outcome
2314                .source_range_to_object
2315                .get(&line_node_ref.range)
2316                .copied()
2317                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2318            let segment_object = outcome
2319                .scene_object_by_id(segment_id)
2320                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2321            let ObjectKind::Segment { segment } = &segment_object.kind else {
2322                return Err(make_err(format!(
2323                    "Object is not a segment, it is {}",
2324                    segment_object.kind.human_friendly_kind_with_article()
2325                )));
2326            };
2327            let Segment::Line(line) = segment else {
2328                return Err(make_err(format!(
2329                    "Segment is not a line, it is {}",
2330                    segment.human_friendly_kind_with_article()
2331                )));
2332            };
2333            vec![line.start, line.end, segment_id]
2334        };
2335        let src_delta = SourceDelta { text: new_source };
2336        // Uses .no_freedom_analysis() so freedom_analysis: false
2337        let outcome = self.update_state_after_exec(outcome, false);
2338        let scene_graph_delta = SceneGraphDelta {
2339            new_graph: self.scene_graph_for_ui(),
2340            invalidates_ids: false,
2341            new_objects: new_object_ids,
2342            exec_outcome: outcome,
2343        };
2344        Ok((src_delta, scene_graph_delta))
2345    }
2346
2347    async fn add_arc(
2348        &mut self,
2349        ctx: &ExecutorContext,
2350        sketch: ObjectId,
2351        ctor: ArcCtor,
2352    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2353        // Create updated KCL source from args.
2354        let start_ast = to_ast_point2d(&ctor.start)
2355            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2356        let end_ast = to_ast_point2d(&ctor.end)
2357            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2358        let center_ast = to_ast_point2d(&ctor.center)
2359            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2360        let mut arguments = vec![
2361            ast::LabeledArg {
2362                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2363                arg: start_ast,
2364            },
2365            ast::LabeledArg {
2366                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2367                arg: end_ast,
2368            },
2369            ast::LabeledArg {
2370                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2371                arg: center_ast,
2372            },
2373        ];
2374        // Add direction kwarg if it's clockwise, since counterclockwise is the
2375        // default.
2376        if ctor.direction == Some(ArcDirection::Cw) {
2377            arguments.push(ast::LabeledArg {
2378                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2379                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2380            });
2381        }
2382        // Add construction kwarg if construction is Some(true)
2383        if ctor.construction == Some(true) {
2384            arguments.push(ast::LabeledArg {
2385                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2386                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2387                    value: ast::LiteralValue::Bool(true),
2388                    raw: "true".to_string(),
2389                    digest: None,
2390                }))),
2391            });
2392        }
2393        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2394            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2395            unlabeled: None,
2396            arguments,
2397            digest: None,
2398            non_code_meta: Default::default(),
2399        })));
2400
2401        // Look up existing sketch.
2402        let sketch_id = sketch;
2403        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2404            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2405        })?;
2406        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2407            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408                "Object is not a sketch, it is {}",
2409                sketch_object.kind.human_friendly_kind_with_article(),
2410            ))));
2411        };
2412        // Add the arc to the AST of the sketch block.
2413        let mut new_ast = self.program.ast.clone();
2414        let (sketch_block_ref, _) = self
2415            .mutate_ast(
2416                &mut new_ast,
2417                sketch_id,
2418                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2419            )
2420            .map_err(KclErrorWithOutputs::no_outputs)?;
2421        // Convert to string source to create real source ranges.
2422        let new_source = source_from_ast(&new_ast);
2423        // Parse the new KCL source.
2424        let new_program = parse_frontend_mutation_source(
2425            &new_source,
2426            "Error parsing KCL source after adding arc",
2427            "No AST produced after adding arc",
2428        )?;
2429
2430        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2431            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2432                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2433            )))
2434        })?;
2435
2436        // Make sure to only set this if there are no errors.
2437        self.program = new_program.clone();
2438
2439        // Truncate after the sketch block for mock execution.
2440        let mut truncated_program = new_program;
2441        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2442            .map_err(KclErrorWithOutputs::no_outputs)?;
2443
2444        // Execute.
2445        let outcome = ctx
2446            .run_mock(
2447                &truncated_program,
2448                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2449            )
2450            .await?;
2451
2452        let new_object_ids = {
2453            let make_err =
2454                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2455            let segment_id = outcome
2456                .source_range_to_object
2457                .get(&arc_node_ref.range)
2458                .copied()
2459                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2460            let segment_object = outcome
2461                .scene_objects
2462                .get(segment_id.0)
2463                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2464            let ObjectKind::Segment { segment } = &segment_object.kind else {
2465                return Err(make_err(format!(
2466                    "Object is not a segment, it is {}",
2467                    segment_object.kind.human_friendly_kind_with_article()
2468                )));
2469            };
2470            let Segment::Arc(arc) = segment else {
2471                return Err(make_err(format!(
2472                    "Segment is not an arc, it is {}",
2473                    segment.human_friendly_kind_with_article()
2474                )));
2475            };
2476            vec![arc.start, arc.end, arc.center, segment_id]
2477        };
2478        let src_delta = SourceDelta { text: new_source };
2479        // Uses .no_freedom_analysis() so freedom_analysis: false
2480        let outcome = self.update_state_after_exec(outcome, false);
2481        let scene_graph_delta = SceneGraphDelta {
2482            new_graph: self.scene_graph_for_ui(),
2483            invalidates_ids: false,
2484            new_objects: new_object_ids,
2485            exec_outcome: outcome,
2486        };
2487        Ok((src_delta, scene_graph_delta))
2488    }
2489
2490    async fn add_circle(
2491        &mut self,
2492        ctx: &ExecutorContext,
2493        sketch: ObjectId,
2494        ctor: CircleCtor,
2495    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2496        // Create updated KCL source from args.
2497        let start_ast = to_ast_point2d(&ctor.start)
2498            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2499        let center_ast = to_ast_point2d(&ctor.center)
2500            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2501        let mut arguments = vec![
2502            ast::LabeledArg {
2503                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2504                arg: start_ast,
2505            },
2506            ast::LabeledArg {
2507                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2508                arg: center_ast,
2509            },
2510        ];
2511        // Add construction kwarg if construction is Some(true)
2512        if ctor.construction == Some(true) {
2513            arguments.push(ast::LabeledArg {
2514                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2515                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2516                    value: ast::LiteralValue::Bool(true),
2517                    raw: "true".to_string(),
2518                    digest: None,
2519                }))),
2520            });
2521        }
2522        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2523            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2524            unlabeled: None,
2525            arguments,
2526            digest: None,
2527            non_code_meta: Default::default(),
2528        })));
2529
2530        // Look up existing sketch.
2531        let sketch_id = sketch;
2532        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2533            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2534        })?;
2535        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2536            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2537                "Object is not a sketch, it is {}",
2538                sketch_object.kind.human_friendly_kind_with_article(),
2539            ))));
2540        };
2541        // Add the circle to the AST of the sketch block.
2542        let mut new_ast = self.program.ast.clone();
2543        let (sketch_block_ref, _) = self
2544            .mutate_ast(
2545                &mut new_ast,
2546                sketch_id,
2547                AstMutateCommand::AddSketchBlockVarDecl {
2548                    prefix: CIRCLE_VARIABLE.to_owned(),
2549                    expr: circle_ast,
2550                },
2551            )
2552            .map_err(KclErrorWithOutputs::no_outputs)?;
2553        // Convert to string source to create real source ranges.
2554        let new_source = source_from_ast(&new_ast);
2555        // Parse the new KCL source.
2556        let new_program = parse_frontend_mutation_source(
2557            &new_source,
2558            "Error parsing KCL source after adding circle",
2559            "No AST produced after adding circle",
2560        )?;
2561
2562        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2563            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2564                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2565            )))
2566        })?;
2567
2568        // Make sure to only set this if there are no errors.
2569        self.program = new_program.clone();
2570
2571        // Truncate after the sketch block for mock execution.
2572        let mut truncated_program = new_program;
2573        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2574            .map_err(KclErrorWithOutputs::no_outputs)?;
2575
2576        // Execute.
2577        let outcome = ctx
2578            .run_mock(
2579                &truncated_program,
2580                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2581            )
2582            .await?;
2583
2584        let new_object_ids = {
2585            let make_err =
2586                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2587            let segment_id = outcome
2588                .source_range_to_object
2589                .get(&circle_node_ref.range)
2590                .copied()
2591                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2592            let segment_object = outcome
2593                .scene_objects
2594                .get(segment_id.0)
2595                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2596            let ObjectKind::Segment { segment } = &segment_object.kind else {
2597                return Err(make_err(format!(
2598                    "Object is not a segment, it is {}",
2599                    segment_object.kind.human_friendly_kind_with_article()
2600                )));
2601            };
2602            let Segment::Circle(circle) = segment else {
2603                return Err(make_err(format!(
2604                    "Segment is not a circle, it is {}",
2605                    segment.human_friendly_kind_with_article()
2606                )));
2607            };
2608            vec![circle.start, circle.center, segment_id]
2609        };
2610        let src_delta = SourceDelta { text: new_source };
2611        // Uses .no_freedom_analysis() so freedom_analysis: false
2612        let outcome = self.update_state_after_exec(outcome, false);
2613        let scene_graph_delta = SceneGraphDelta {
2614            new_graph: self.scene_graph_for_ui(),
2615            invalidates_ids: false,
2616            new_objects: new_object_ids,
2617            exec_outcome: outcome,
2618        };
2619        Ok((src_delta, scene_graph_delta))
2620    }
2621
2622    async fn add_control_point_spline(
2623        &mut self,
2624        ctx: &ExecutorContext,
2625        sketch: ObjectId,
2626        ctor: ControlPointSplineCtor,
2627    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2628        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2629            .map_err(KclErrorWithOutputs::no_outputs)?;
2630
2631        let points_ast = to_ast_point2d_array(&ctor.points)
2632            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2633        let mut arguments = vec![ast::LabeledArg {
2634            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2635            arg: points_ast,
2636        }];
2637        if ctor.construction == Some(true) {
2638            arguments.push(ast::LabeledArg {
2639                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2640                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2641                    value: ast::LiteralValue::Bool(true),
2642                    raw: "true".to_string(),
2643                    digest: None,
2644                }))),
2645            });
2646        }
2647        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2648            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2649            unlabeled: None,
2650            arguments,
2651            digest: None,
2652            non_code_meta: Default::default(),
2653        })));
2654
2655        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2656            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2657        })?;
2658        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2659            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660                "Object is not a sketch, it is {}",
2661                sketch_object.kind.human_friendly_kind_with_article(),
2662            ))));
2663        };
2664
2665        let mut new_ast = new_program.ast.clone();
2666        let (sketch_block_ref, _) = self
2667            .mutate_ast(
2668                &mut new_ast,
2669                sketch,
2670                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2671            )
2672            .map_err(KclErrorWithOutputs::no_outputs)?;
2673        let new_source = source_from_ast(&new_ast);
2674        let new_program = parse_frontend_mutation_source(
2675            &new_source,
2676            "Error parsing KCL source after adding controlPointSpline",
2677            "No AST produced after adding controlPointSpline",
2678        )?;
2679
2680        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2681            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2682                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2683            )))
2684        })?;
2685
2686        self.program = new_program.clone();
2687
2688        let mut truncated_program = new_program;
2689        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2690            .map_err(KclErrorWithOutputs::no_outputs)?;
2691
2692        let outcome = ctx
2693            .run_mock(
2694                &truncated_program,
2695                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2696            )
2697            .await?;
2698
2699        let new_object_ids = {
2700            let make_err =
2701                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2702            let segment_id = outcome
2703                .source_range_to_object
2704                .get(&spline_node_ref.range)
2705                .copied()
2706                .ok_or_else(|| {
2707                    make_err(format!(
2708                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2709                    ))
2710                })?;
2711            let segment_object = outcome
2712                .scene_objects
2713                .get(segment_id.0)
2714                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2715            let ObjectKind::Segment { segment } = &segment_object.kind else {
2716                return Err(make_err(format!(
2717                    "Object is not a segment, it is {}",
2718                    segment_object.kind.human_friendly_kind_with_article()
2719                )));
2720            };
2721            let Segment::ControlPointSpline(spline) = segment else {
2722                return Err(make_err(format!(
2723                    "Segment is not a control point spline, it is {}",
2724                    segment.human_friendly_kind_with_article()
2725                )));
2726            };
2727
2728            let mut ids = outcome
2729                .scene_objects
2730                .iter()
2731                .filter_map(|obj| match &obj.kind {
2732                    ObjectKind::Segment {
2733                        segment: Segment::Line(line),
2734                    } if line.owner == Some(segment_id) => Some(obj.id),
2735                    _ => None,
2736                })
2737                .collect::<Vec<_>>();
2738            ids.extend(spline.controls.clone());
2739            ids.push(segment_id);
2740            ids
2741        };
2742        let src_delta = SourceDelta { text: new_source };
2743        let outcome = self.update_state_after_exec(outcome, false);
2744        let scene_graph_delta = SceneGraphDelta {
2745            new_graph: self.scene_graph_for_ui(),
2746            invalidates_ids: false,
2747            new_objects: new_object_ids,
2748            exec_outcome: outcome,
2749        };
2750        Ok((src_delta, scene_graph_delta))
2751    }
2752
2753    fn edit_point(
2754        &mut self,
2755        new_ast: &mut ast::Node<ast::Program>,
2756        sketch: ObjectId,
2757        point: ObjectId,
2758        ctor: PointCtor,
2759    ) -> Result<(), KclError> {
2760        // Create updated KCL source from args.
2761        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2762
2763        // Look up existing sketch.
2764        let sketch_id = sketch;
2765        let sketch_object = self
2766            .scene_graph
2767            .objects
2768            .get(sketch_id.0)
2769            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2770        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2771            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2772        };
2773        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2774            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2775        })?;
2776        // Look up existing point.
2777        let point_id = point;
2778        let point_object = self
2779            .scene_graph
2780            .objects
2781            .get(point_id.0)
2782            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2783        let ObjectKind::Segment {
2784            segment: Segment::Point(point),
2785        } = &point_object.kind
2786        else {
2787            return Err(KclError::refactor(format!(
2788                "Object is not a point segment: {point_object:?}"
2789            )));
2790        };
2791
2792        // If the point is part of a line or arc, edit the line/arc instead.
2793        if let Some(owner_id) = point.owner {
2794            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2795                KclError::refactor(format!(
2796                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2797                ))
2798            })?;
2799            let ObjectKind::Segment { segment } = &owner_object.kind else {
2800                return Err(KclError::refactor(format!(
2801                    "Internal: Owner of point is not a segment, but found {}",
2802                    owner_object.kind.human_friendly_kind_with_article()
2803                )));
2804            };
2805
2806            // Handle Line owner
2807            if let Segment::Line(line) = segment {
2808                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2809                    return Err(KclError::refactor(format!(
2810                        "Internal: Owner of point does not have line ctor, but found {}",
2811                        line.ctor.human_friendly_kind_with_article()
2812                    )));
2813                };
2814                let mut line_ctor = line_ctor.clone();
2815                // Which end of the line is this point?
2816                if line.start == point_id {
2817                    line_ctor.start = ctor.position;
2818                } else if line.end == point_id {
2819                    line_ctor.end = ctor.position;
2820                } else {
2821                    return Err(KclError::refactor(format!(
2822                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2823                    )));
2824                }
2825                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2826            }
2827
2828            // Handle Arc owner
2829            if let Segment::Arc(arc) = segment {
2830                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2831                    return Err(KclError::refactor(format!(
2832                        "Internal: Owner of point does not have arc ctor, but found {}",
2833                        arc.ctor.human_friendly_kind_with_article()
2834                    )));
2835                };
2836                let mut arc_ctor = arc_ctor.clone();
2837                // Which point of the arc is this? (center, start, or end)
2838                if arc.center == point_id {
2839                    arc_ctor.center = ctor.position;
2840                } else if arc.start == point_id {
2841                    arc_ctor.start = ctor.position;
2842                } else if arc.end == point_id {
2843                    arc_ctor.end = ctor.position;
2844                } else {
2845                    return Err(KclError::refactor(format!(
2846                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2847                    )));
2848                }
2849                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2850            }
2851
2852            // Handle Circle owner
2853            if let Segment::Circle(circle) = segment {
2854                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2855                    return Err(KclError::refactor(format!(
2856                        "Internal: Owner of point does not have circle ctor, but found {}",
2857                        circle.ctor.human_friendly_kind_with_article()
2858                    )));
2859                };
2860                let mut circle_ctor = circle_ctor.clone();
2861                if circle.center == point_id {
2862                    circle_ctor.center = ctor.position;
2863                } else if circle.start == point_id {
2864                    circle_ctor.start = ctor.position;
2865                } else {
2866                    return Err(KclError::refactor(format!(
2867                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2868                    )));
2869                }
2870                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2871            }
2872
2873            if let Segment::ControlPointSpline(spline) = segment {
2874                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2875                    return Err(KclError::refactor(format!(
2876                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2877                        spline.ctor.human_friendly_kind_with_article()
2878                    )));
2879                };
2880                let mut spline_ctor = spline_ctor.clone();
2881                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2882                    return Err(KclError::refactor(format!(
2883                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2884                    )));
2885                };
2886                spline_ctor.points[control_index] = ctor.position;
2887                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2888            }
2889
2890            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2891            // (fall through to the point editing logic below)
2892        }
2893
2894        // Modify the point AST.
2895        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2896        Ok(())
2897    }
2898
2899    fn edit_line(
2900        &mut self,
2901        new_ast: &mut ast::Node<ast::Program>,
2902        sketch: ObjectId,
2903        line: ObjectId,
2904        ctor: LineCtor,
2905    ) -> Result<(), KclError> {
2906        // Create updated KCL source from args.
2907        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2908        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2909
2910        // Look up existing sketch.
2911        let sketch_id = sketch;
2912        let sketch_object = self
2913            .scene_graph
2914            .objects
2915            .get(sketch_id.0)
2916            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2917        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2918            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2919        };
2920        sketch
2921            .segments
2922            .iter()
2923            .find(|o| **o == line)
2924            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2925        // Look up existing line.
2926        let line_id = line;
2927        let line_object = self
2928            .scene_graph
2929            .objects
2930            .get(line_id.0)
2931            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2932        let ObjectKind::Segment { .. } = &line_object.kind else {
2933            let kind = line_object.kind.human_friendly_kind_with_article();
2934            return Err(KclError::refactor(format!(
2935                "This constraint only works on Segments, but you selected {kind}"
2936            )));
2937        };
2938
2939        // Modify the line AST.
2940        self.mutate_ast(
2941            new_ast,
2942            line_id,
2943            AstMutateCommand::EditLine {
2944                start: new_start_ast,
2945                end: new_end_ast,
2946                construction: ctor.construction,
2947            },
2948        )?;
2949        Ok(())
2950    }
2951
2952    fn edit_arc(
2953        &mut self,
2954        new_ast: &mut ast::Node<ast::Program>,
2955        sketch: ObjectId,
2956        arc: ObjectId,
2957        ctor: ArcCtor,
2958    ) -> Result<(), KclError> {
2959        // Create updated KCL source from args.
2960        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2961        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2962        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2963
2964        // Look up existing sketch.
2965        let sketch_id = sketch;
2966        let sketch_object = self
2967            .scene_graph
2968            .objects
2969            .get(sketch_id.0)
2970            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2971        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2972            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2973        };
2974        sketch
2975            .segments
2976            .iter()
2977            .find(|o| **o == arc)
2978            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2979        // Look up existing arc.
2980        let arc_id = arc;
2981        let arc_object = self
2982            .scene_graph
2983            .objects
2984            .get(arc_id.0)
2985            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2986        let ObjectKind::Segment { .. } = &arc_object.kind else {
2987            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2988        };
2989
2990        // Modify the arc AST.
2991        self.mutate_ast(
2992            new_ast,
2993            arc_id,
2994            AstMutateCommand::EditArc {
2995                start: new_start_ast,
2996                end: new_end_ast,
2997                center: new_center_ast,
2998                direction: ctor.direction,
2999                construction: ctor.construction,
3000            },
3001        )?;
3002        Ok(())
3003    }
3004
3005    fn edit_circle(
3006        &mut self,
3007        new_ast: &mut ast::Node<ast::Program>,
3008        sketch: ObjectId,
3009        circle: ObjectId,
3010        ctor: CircleCtor,
3011    ) -> Result<(), KclError> {
3012        // Create updated KCL source from args.
3013        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3014        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3015
3016        // Look up existing sketch.
3017        let sketch_id = sketch;
3018        let sketch_object = self
3019            .scene_graph
3020            .objects
3021            .get(sketch_id.0)
3022            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3023        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3024            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3025        };
3026        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3027            KclError::refactor(format!(
3028                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3029            ))
3030        })?;
3031        // Look up existing circle.
3032        let circle_id = circle;
3033        let circle_object = self
3034            .scene_graph
3035            .objects
3036            .get(circle_id.0)
3037            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3038        let ObjectKind::Segment { .. } = &circle_object.kind else {
3039            return Err(KclError::refactor(format!(
3040                "Object is not a segment: {circle_object:?}"
3041            )));
3042        };
3043
3044        // Modify the circle AST.
3045        self.mutate_ast(
3046            new_ast,
3047            circle_id,
3048            AstMutateCommand::EditCircle {
3049                start: new_start_ast,
3050                center: new_center_ast,
3051                construction: ctor.construction,
3052            },
3053        )?;
3054        Ok(())
3055    }
3056
3057    fn edit_control_point_spline(
3058        &mut self,
3059        new_ast: &mut ast::Node<ast::Program>,
3060        sketch: ObjectId,
3061        spline: ObjectId,
3062        ctor: ControlPointSplineCtor,
3063    ) -> Result<(), KclError> {
3064        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3065
3066        let sketch_object = self
3067            .scene_graph
3068            .objects
3069            .get(sketch.0)
3070            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3071        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3072            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3073        };
3074        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3075            KclError::refactor(format!(
3076                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3077            ))
3078        })?;
3079
3080        let spline_object =
3081            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3082                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3083            })?;
3084        let ObjectKind::Segment { .. } = &spline_object.kind else {
3085            return Err(KclError::refactor(format!(
3086                "Object is not a segment: {spline_object:?}"
3087            )));
3088        };
3089
3090        self.mutate_ast(
3091            new_ast,
3092            spline,
3093            AstMutateCommand::EditControlPointSpline {
3094                points: points_ast,
3095                construction: ctor.construction,
3096            },
3097        )?;
3098        Ok(())
3099    }
3100
3101    fn delete_segment(
3102        &mut self,
3103        new_ast: &mut ast::Node<ast::Program>,
3104        sketch: ObjectId,
3105        segment_id: ObjectId,
3106    ) -> Result<(), KclError> {
3107        // Look up existing sketch.
3108        let sketch_id = sketch;
3109        let sketch_object = self
3110            .scene_graph
3111            .objects
3112            .get(sketch_id.0)
3113            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3114        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3115            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3116        };
3117        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3118            KclError::refactor(format!(
3119                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3120            ))
3121        })?;
3122        // Look up existing segment.
3123        let segment_object =
3124            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3125                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3126            })?;
3127        let ObjectKind::Segment { .. } = &segment_object.kind else {
3128            return Err(KclError::refactor(format!(
3129                "Object is not a segment, it is {}",
3130                segment_object.kind.human_friendly_kind_with_article()
3131            )));
3132        };
3133
3134        // Modify the AST to remove the segment.
3135        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3136        Ok(())
3137    }
3138
3139    fn delete_constraint(
3140        &mut self,
3141        new_ast: &mut ast::Node<ast::Program>,
3142        sketch: ObjectId,
3143        constraint_id: ObjectId,
3144    ) -> Result<(), KclError> {
3145        // Look up existing sketch.
3146        let sketch_id = sketch;
3147        let sketch_object = self
3148            .scene_graph
3149            .objects
3150            .get(sketch_id.0)
3151            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3152        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3153            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3154        };
3155        sketch
3156            .constraints
3157            .iter()
3158            .find(|o| **o == constraint_id)
3159            .ok_or_else(|| {
3160                KclError::refactor(format!(
3161                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3162                ))
3163            })?;
3164        // Look up existing constraint.
3165        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3166            KclError::refactor(format!(
3167                "Constraint not found in scene graph: constraint={constraint_id:?}"
3168            ))
3169        })?;
3170        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3171            return Err(KclError::refactor(format!(
3172                "Object is not a constraint, it is {}",
3173                constraint_object.kind.human_friendly_kind_with_article()
3174            )));
3175        };
3176
3177        // Modify the AST to remove the constraint.
3178        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3179        Ok(())
3180    }
3181
3182    fn edit_coincident_constraint(
3183        &mut self,
3184        new_ast: &mut ast::Node<ast::Program>,
3185        constraint_id: ObjectId,
3186        segments: Vec<ConstraintSegment>,
3187    ) -> Result<(), KclError> {
3188        if segments.len() < 2 {
3189            return Err(KclError::refactor(format!(
3190                "Coincident constraint must have at least 2 inputs, got {}",
3191                segments.len()
3192            )));
3193        }
3194
3195        let segment_asts = segments
3196            .iter()
3197            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3198            .collect::<Result<Vec<_>, _>>()?;
3199
3200        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3201            elements: segment_asts,
3202            digest: None,
3203            non_code_meta: Default::default(),
3204        })));
3205
3206        self.mutate_ast(
3207            new_ast,
3208            constraint_id,
3209            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3210        )?;
3211        Ok(())
3212    }
3213
3214    fn edit_horizontal_points_constraint(
3215        &mut self,
3216        new_ast: &mut ast::Node<ast::Program>,
3217        constraint_id: ObjectId,
3218        points: Vec<ConstraintSegment>,
3219    ) -> Result<(), KclError> {
3220        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3221    }
3222
3223    fn edit_vertical_points_constraint(
3224        &mut self,
3225        new_ast: &mut ast::Node<ast::Program>,
3226        constraint_id: ObjectId,
3227        points: Vec<ConstraintSegment>,
3228    ) -> Result<(), KclError> {
3229        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3230    }
3231
3232    fn edit_axis_points_constraint(
3233        &mut self,
3234        new_ast: &mut ast::Node<ast::Program>,
3235        constraint_id: ObjectId,
3236        points: Vec<ConstraintSegment>,
3237        constraint_name: &str,
3238    ) -> Result<(), KclError> {
3239        if points.len() < 2 {
3240            return Err(KclError::refactor(format!(
3241                "{constraint_name} points constraint must have at least 2 points, got {}",
3242                points.len()
3243            )));
3244        }
3245
3246        let point_asts = points
3247            .iter()
3248            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3249            .collect::<Result<Vec<_>, _>>()?;
3250
3251        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3252            elements: point_asts,
3253            digest: None,
3254            non_code_meta: Default::default(),
3255        })));
3256
3257        self.mutate_ast(
3258            new_ast,
3259            constraint_id,
3260            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3261        )?;
3262        Ok(())
3263    }
3264
3265    /// updates the equalLength constraint with the given lines
3266    fn edit_equal_length_constraint(
3267        &mut self,
3268        new_ast: &mut ast::Node<ast::Program>,
3269        constraint_id: ObjectId,
3270        lines: Vec<ObjectId>,
3271    ) -> Result<(), KclError> {
3272        if lines.len() < 2 {
3273            return Err(KclError::refactor(format!(
3274                "Lines equal length constraint must have at least 2 lines, got {}",
3275                lines.len()
3276            )));
3277        }
3278
3279        let line_asts = lines
3280            .iter()
3281            .map(|line_id| {
3282                let line_object = self
3283                    .scene_graph
3284                    .objects
3285                    .get(line_id.0)
3286                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3287                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3288                    let kind = line_object.kind.human_friendly_kind_with_article();
3289                    return Err(KclError::refactor(format!(
3290                        "This constraint only works on Segments, but you selected {kind}"
3291                    )));
3292                };
3293                let Segment::Line(_) = line_segment else {
3294                    let kind = line_segment.human_friendly_kind_with_article();
3295                    return Err(KclError::refactor(format!(
3296                        "Only lines can be made equal length, but you selected {kind}"
3297                    )));
3298                };
3299
3300                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3301            })
3302            .collect::<Result<Vec<_>, _>>()?;
3303
3304        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3305            elements: line_asts,
3306            digest: None,
3307            non_code_meta: Default::default(),
3308        })));
3309
3310        self.mutate_ast(
3311            new_ast,
3312            constraint_id,
3313            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3314        )?;
3315        Ok(())
3316    }
3317
3318    /// Updates the parallel constraint with the given lines.
3319    fn edit_parallel_constraint(
3320        &mut self,
3321        new_ast: &mut ast::Node<ast::Program>,
3322        constraint_id: ObjectId,
3323        lines: Vec<ObjectId>,
3324    ) -> Result<(), KclError> {
3325        if lines.len() < 2 {
3326            return Err(KclError::refactor(format!(
3327                "Parallel constraint must have at least 2 lines, got {}",
3328                lines.len()
3329            )));
3330        }
3331
3332        let line_asts = lines
3333            .iter()
3334            .map(|line_id| {
3335                let line_object = self
3336                    .scene_graph
3337                    .objects
3338                    .get(line_id.0)
3339                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3340                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3341                    let kind = line_object.kind.human_friendly_kind_with_article();
3342                    return Err(KclError::refactor(format!(
3343                        "This constraint only works on Segments, but you selected {kind}"
3344                    )));
3345                };
3346                let Segment::Line(_) = line_segment else {
3347                    let kind = line_segment.human_friendly_kind_with_article();
3348                    return Err(KclError::refactor(format!(
3349                        "Only lines can be made parallel, but you selected {kind}"
3350                    )));
3351                };
3352
3353                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3354            })
3355            .collect::<Result<Vec<_>, _>>()?;
3356
3357        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3358            elements: line_asts,
3359            digest: None,
3360            non_code_meta: Default::default(),
3361        })));
3362
3363        self.mutate_ast(
3364            new_ast,
3365            constraint_id,
3366            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3367        )?;
3368        Ok(())
3369    }
3370
3371    /// Updates the equalRadius constraint with the given segments.
3372    fn edit_equal_radius_constraint(
3373        &mut self,
3374        new_ast: &mut ast::Node<ast::Program>,
3375        constraint_id: ObjectId,
3376        input: Vec<ObjectId>,
3377    ) -> Result<(), KclError> {
3378        if input.len() < 2 {
3379            return Err(KclError::refactor(format!(
3380                "equalRadius constraint must have at least 2 segments, got {}",
3381                input.len()
3382            )));
3383        }
3384
3385        let input_asts = input
3386            .iter()
3387            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3388            .collect::<Result<Vec<_>, _>>()?;
3389
3390        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3391            elements: input_asts,
3392            digest: None,
3393            non_code_meta: Default::default(),
3394        })));
3395
3396        self.mutate_ast(
3397            new_ast,
3398            constraint_id,
3399            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3400        )?;
3401        Ok(())
3402    }
3403
3404    async fn execute_after_edit(
3405        &mut self,
3406        ctx: &ExecutorContext,
3407        sketch: ObjectId,
3408        sketch_block_ref: AstNodeRef,
3409        new_ast: &mut ast::Node<ast::Program>,
3410        options: ExecuteAfterEditOptions,
3411    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3412        let ExecuteAfterEditOptions {
3413            segment_ids_edited,
3414            edit_kind,
3415            commit_solved_initial_guesses,
3416        } = options;
3417
3418        // Convert to string source to create real source ranges.
3419        let new_source = source_from_ast(new_ast);
3420        // Parse the new KCL source.
3421        let new_program = parse_frontend_mutation_source(
3422            &new_source,
3423            "Error parsing KCL source after editing",
3424            "No AST produced after editing",
3425        )?;
3426
3427        // Truncate after the sketch block for mock execution.
3428        let is_delete = edit_kind.is_delete();
3429        let truncated_program = {
3430            let mut truncated_program = new_program.clone();
3431            only_sketch_block(
3432                &mut truncated_program.ast,
3433                &sketch_block_ref,
3434                edit_kind.to_change_kind(),
3435            )
3436            .map_err(KclErrorWithOutputs::no_outputs)?;
3437            truncated_program
3438        };
3439
3440        // Execute.
3441        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3442        let mock_config = MockConfig {
3443            sketch_block_id: Some(sketch),
3444            freedom_analysis: is_delete,
3445            segment_ids_edited: segment_ids_edited.clone(),
3446            drag_anchors,
3447            ..Default::default()
3448        };
3449        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3450
3451        // Only now, after execution has succeeded, update self.program.
3452        self.program = new_program;
3453
3454        // Uses freedom_analysis: is_delete
3455        let outcome = self.update_state_after_exec(outcome, is_delete);
3456
3457        let src_delta = if commit_solved_initial_guesses {
3458            self.commit_var_solutions_to_program(&outcome, "editing")?
3459        } else {
3460            SourceDelta { text: new_source }
3461        };
3462        let scene_graph_delta = SceneGraphDelta {
3463            new_graph: self.scene_graph_for_ui(),
3464            invalidates_ids: is_delete,
3465            new_objects: Vec::new(),
3466            exec_outcome: outcome,
3467        };
3468        Ok((src_delta, scene_graph_delta))
3469    }
3470
3471    async fn execute_after_delete_sketch(
3472        &mut self,
3473        ctx: &ExecutorContext,
3474        new_ast: &mut ast::Node<ast::Program>,
3475    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3476        // Convert to string source to create real source ranges.
3477        let new_source = source_from_ast(new_ast);
3478        // Parse the new KCL source.
3479        let new_program = parse_frontend_mutation_source(
3480            &new_source,
3481            "Error parsing KCL source after editing",
3482            "No AST produced after editing",
3483        )?;
3484
3485        // Make sure to only set this if there are no errors.
3486        self.program = new_program.clone();
3487
3488        // We deleted the entire sketch block. It doesn't make sense to truncate
3489        // and execute only the sketch block. We execute the whole program with
3490        // a real engine.
3491
3492        // Execute.
3493        let outcome = ctx.run_with_caching(new_program).await?;
3494        let freedom_analysis_ran = true;
3495
3496        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3497
3498        let src_delta = SourceDelta { text: new_source };
3499        let scene_graph_delta = SceneGraphDelta {
3500            new_graph: self.scene_graph_for_ui(),
3501            invalidates_ids: true,
3502            new_objects: Vec::new(),
3503            exec_outcome: outcome,
3504        };
3505        Ok((src_delta, scene_graph_delta))
3506    }
3507
3508    /// Map a point object id into an AST reference expression for use in
3509    /// constraints. If the point is owned by a segment (line or arc), we
3510    /// reference the appropriate property on that segment (e.g. `line1.start`,
3511    /// `arc1.center`). Otherwise we reference the point directly.
3512    fn point_id_to_ast_reference(
3513        &self,
3514        point_id: ObjectId,
3515        new_ast: &mut ast::Node<ast::Program>,
3516    ) -> Result<ast::Expr, KclError> {
3517        let point_object = self
3518            .scene_graph
3519            .objects
3520            .get(point_id.0)
3521            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3522        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3523            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3524        };
3525        let Segment::Point(point) = point_segment else {
3526            return Err(KclError::refactor(format!(
3527                "Only points are currently supported: {point_object:?}"
3528            )));
3529        };
3530
3531        if let Some(owner_id) = point.owner {
3532            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3533                KclError::refactor(format!(
3534                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3535                ))
3536            })?;
3537            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3538                return Err(KclError::refactor(format!(
3539                    "Owner of point is not a segment, but found {}",
3540                    owner_object.kind.human_friendly_kind_with_article()
3541                )));
3542            };
3543
3544            match owner_segment {
3545                Segment::Line(line) => {
3546                    let property = if line.start == point_id {
3547                        LINE_PROPERTY_START
3548                    } else if line.end == point_id {
3549                        LINE_PROPERTY_END
3550                    } else {
3551                        return Err(KclError::refactor(format!(
3552                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3553                        )));
3554                    };
3555                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3556                }
3557                Segment::Arc(arc) => {
3558                    let property = if arc.start == point_id {
3559                        ARC_PROPERTY_START
3560                    } else if arc.end == point_id {
3561                        ARC_PROPERTY_END
3562                    } else if arc.center == point_id {
3563                        ARC_PROPERTY_CENTER
3564                    } else {
3565                        return Err(KclError::refactor(format!(
3566                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3567                        )));
3568                    };
3569                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3570                }
3571                Segment::Circle(circle) => {
3572                    let property = if circle.start == point_id {
3573                        CIRCLE_PROPERTY_START
3574                    } else if circle.center == point_id {
3575                        CIRCLE_PROPERTY_CENTER
3576                    } else {
3577                        return Err(KclError::refactor(format!(
3578                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3579                        )));
3580                    };
3581                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3582                }
3583                Segment::ControlPointSpline(spline) => {
3584                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3585                        return Err(KclError::refactor(format!(
3586                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3587                        )));
3588                    };
3589                    let owner_expr =
3590                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3591                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3592                    Ok(create_index_expression(controls_expr, index))
3593                }
3594                _ => Err(KclError::refactor(format!(
3595                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3596                ))),
3597            }
3598        } else {
3599            // Standalone point.
3600            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3601        }
3602    }
3603
3604    fn line_id_to_ast_reference(
3605        &self,
3606        line_id: ObjectId,
3607        new_ast: &mut ast::Node<ast::Program>,
3608    ) -> Result<ast::Expr, KclError> {
3609        let line_object = self
3610            .scene_graph
3611            .objects
3612            .get(line_id.0)
3613            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3614        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3615            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3616        };
3617        let Segment::Line(line) = line_segment else {
3618            return Err(KclError::refactor(format!(
3619                "Only lines are currently supported: {line_object:?}"
3620            )));
3621        };
3622
3623        if let Some(owner_id) = line.owner {
3624            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3625                KclError::refactor(format!(
3626                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3627                ))
3628            })?;
3629            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3630                return Err(KclError::refactor(format!(
3631                    "Owner of line is not a segment, but found {}",
3632                    owner_object.kind.human_friendly_kind_with_article()
3633                )));
3634            };
3635
3636            match owner_segment {
3637                Segment::ControlPointSpline(spline) => {
3638                    let Some(index) = spline
3639                        .controls
3640                        .windows(2)
3641                        .position(|window| window[0] == line.start && window[1] == line.end)
3642                    else {
3643                        return Err(KclError::refactor(format!(
3644                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3645                        )));
3646                    };
3647                    let owner_expr =
3648                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3649                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3650                    Ok(create_index_expression(edges_expr, index))
3651                }
3652                _ => Err(KclError::refactor(format!(
3653                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3654                ))),
3655            }
3656        } else {
3657            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3658        }
3659    }
3660
3661    fn coincident_segment_to_ast(
3662        &self,
3663        segment: &ConstraintSegment,
3664        new_ast: &mut ast::Node<ast::Program>,
3665    ) -> Result<ast::Expr, KclError> {
3666        match segment {
3667            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3668            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3669        }
3670    }
3671
3672    fn segment_id_to_constraint_ast_reference(
3673        &self,
3674        segment_id: ObjectId,
3675        new_ast: &mut ast::Node<ast::Program>,
3676    ) -> Result<ast::Expr, KclError> {
3677        let segment_object = self
3678            .scene_graph
3679            .objects
3680            .get(segment_id.0)
3681            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3682        let ObjectKind::Segment { segment } = &segment_object.kind else {
3683            return Err(KclError::refactor(format!(
3684                "Object is not a segment, it is {}",
3685                segment_object.kind.human_friendly_kind_with_article()
3686            )));
3687        };
3688
3689        match segment {
3690            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3691            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3692            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3693            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3694            Segment::ControlPointSpline(_) => {
3695                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3696            }
3697        }
3698    }
3699
3700    fn axis_constraint_segment_to_ast(
3701        &self,
3702        segment: &ConstraintSegment,
3703        new_ast: &mut ast::Node<ast::Program>,
3704    ) -> Result<ast::Expr, KclError> {
3705        match segment {
3706            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3707            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3708        }
3709    }
3710
3711    async fn add_coincident(
3712        &mut self,
3713        sketch: ObjectId,
3714        coincident: Coincident,
3715        new_ast: &mut ast::Node<ast::Program>,
3716    ) -> Result<AstNodeRef, KclError> {
3717        let sketch_id = sketch;
3718        for segment in &coincident.segments {
3719            let ConstraintSegment::Segment(segment_id) = segment else {
3720                continue;
3721            };
3722            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3723                continue;
3724            };
3725            if matches!(
3726                segment_object.kind,
3727                ObjectKind::Segment {
3728                    segment: Segment::ControlPointSpline(_)
3729                }
3730            ) {
3731                return Err(KclError::refactor(
3732                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3733                        .to_owned(),
3734                ));
3735            }
3736        }
3737        let segment_asts = coincident
3738            .segments
3739            .iter()
3740            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3741            .collect::<Result<Vec<_>, _>>()?;
3742        if segment_asts.len() < 2 {
3743            return Err(KclError::refactor(format!(
3744                "Coincident constraint must have at least 2 inputs, got {}",
3745                segment_asts.len()
3746            )));
3747        }
3748
3749        // Create the coincident() call using shared helper.
3750        let coincident_ast = create_coincident_ast(segment_asts);
3751
3752        // Add the line to the AST of the sketch block.
3753        let (sketch_block_ref, _) = self.mutate_ast(
3754            new_ast,
3755            sketch_id,
3756            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3757        )?;
3758        Ok(sketch_block_ref)
3759    }
3760
3761    async fn add_distance(
3762        &mut self,
3763        sketch: ObjectId,
3764        distance: Distance,
3765        new_ast: &mut ast::Node<ast::Program>,
3766    ) -> Result<AstNodeRef, KclError> {
3767        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3768    }
3769
3770    fn distance_constraint_ast_parts(
3771        &self,
3772        function_name: &str,
3773        distance: &Distance,
3774        new_ast: &mut ast::Node<ast::Program>,
3775    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3776        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3777            [pt0, pt1] => [
3778                self.coincident_segment_to_ast(pt0, new_ast)?,
3779                self.coincident_segment_to_ast(pt1, new_ast)?,
3780            ],
3781            _ => {
3782                return Err(KclError::refactor(format!(
3783                    "Distance constraint must have exactly 2 segments, got {}",
3784                    distance.segments.len()
3785                )));
3786            }
3787        };
3788
3789        let arguments = match &distance.label_position {
3790            Some(label_position) => vec![ast::LabeledArg {
3791                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3792                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3793            }],
3794            None => Default::default(),
3795        };
3796
3797        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3798            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3799            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3800                ast::ArrayExpression {
3801                    elements: vec![segment0_ast, segment1_ast],
3802                    digest: None,
3803                    non_code_meta: Default::default(),
3804                },
3805            )))),
3806            arguments,
3807            digest: None,
3808            non_code_meta: Default::default(),
3809        })));
3810        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3811            value: ast::LiteralValue::Number {
3812                value: distance.distance.value,
3813                suffix: distance.distance.units,
3814            },
3815            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3816                KclError::refactor(format!(
3817                    "Could not format numeric suffix: {:?}",
3818                    distance.distance.units
3819                ))
3820            })?,
3821            digest: None,
3822        })));
3823
3824        Ok((call, value))
3825    }
3826
3827    fn add_distance_constraint(
3828        &mut self,
3829        sketch: ObjectId,
3830        function_name: &str,
3831        distance: Distance,
3832        new_ast: &mut ast::Node<ast::Program>,
3833    ) -> Result<AstNodeRef, KclError> {
3834        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3835        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3836            left: call,
3837            operator: ast::BinaryOperator::Eq,
3838            right: value,
3839            digest: None,
3840        })));
3841
3842        let (sketch_block_ref, _) = self.mutate_ast(
3843            new_ast,
3844            sketch,
3845            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3846        )?;
3847        Ok(sketch_block_ref)
3848    }
3849
3850    async fn add_angle(
3851        &mut self,
3852        sketch: ObjectId,
3853        angle: Angle,
3854        new_ast: &mut ast::Node<ast::Program>,
3855    ) -> Result<AstNodeRef, KclError> {
3856        let sketch_id = sketch;
3857        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3858        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3859            left: angle_call_ast,
3860            operator: ast::BinaryOperator::Eq,
3861            right: angle_value_ast,
3862            digest: None,
3863        })));
3864
3865        // Add the line to the AST of the sketch block.
3866        let (sketch_block_ref, _) = self.mutate_ast(
3867            new_ast,
3868            sketch_id,
3869            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3870        )?;
3871        Ok(sketch_block_ref)
3872    }
3873
3874    fn angle_constraint_ast_parts(
3875        &self,
3876        angle: &Angle,
3877        new_ast: &mut ast::Node<ast::Program>,
3878    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3879        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3880            return Err(KclError::refactor(format!(
3881                "Angle constraint must have exactly 2 lines, got {}",
3882                angle.lines.len()
3883            )));
3884        };
3885
3886        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3887        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3888        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3889            elements: vec![l0_ast, l1_ast],
3890            digest: None,
3891            non_code_meta: Default::default(),
3892        })));
3893
3894        if angle.inverse == Some(true) && angle.sector.is_none() {
3895            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3896        }
3897
3898        let uses_angle_dimension = angle.sector.is_some();
3899        let mut arguments = if uses_angle_dimension {
3900            vec![ast::LabeledArg {
3901                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3902                arg: lines_ast.clone(),
3903            }]
3904        } else {
3905            Default::default()
3906        };
3907
3908        if let Some(sector) = angle.sector {
3909            arguments.push(ast::LabeledArg {
3910                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3911                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3912                    value: ast::LiteralValue::Number {
3913                        value: f64::from(sector),
3914                        suffix: NumericSuffix::None,
3915                    },
3916                    raw: sector.to_string(),
3917                    digest: None,
3918                }))),
3919            });
3920        }
3921
3922        if angle.inverse == Some(true) {
3923            arguments.push(ast::LabeledArg {
3924                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3925                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3926                    value: ast::LiteralValue::Bool(true),
3927                    raw: true.to_string(),
3928                    digest: None,
3929                }))),
3930            });
3931        }
3932
3933        if let Some(label_position) = &angle.label_position {
3934            arguments.push(ast::LabeledArg {
3935                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3936                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3937            });
3938        }
3939
3940        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3941            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3942                ANGLE_DIMENSION_FN
3943            } else {
3944                ANGLE_FN
3945            })),
3946            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3947            arguments,
3948            digest: None,
3949            non_code_meta: Default::default(),
3950        })));
3951        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3952            value: ast::LiteralValue::Number {
3953                value: angle.angle.value,
3954                suffix: angle.angle.units,
3955            },
3956            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3957                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3958            digest: None,
3959        })));
3960
3961        Ok((call, value))
3962    }
3963
3964    async fn add_tangent(
3965        &mut self,
3966        sketch: ObjectId,
3967        tangent: Tangent,
3968        new_ast: &mut ast::Node<ast::Program>,
3969    ) -> Result<AstNodeRef, KclError> {
3970        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3971            return Err(KclError::refactor(format!(
3972                "Tangent constraint must have exactly 2 segments, got {}",
3973                tangent.input.len()
3974            )));
3975        };
3976        let sketch_id = sketch;
3977
3978        let seg0_object = self
3979            .scene_graph
3980            .objects
3981            .get(seg0_id.0)
3982            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3983        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3984            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3985        };
3986        let seg0_ast = match seg0_segment {
3987            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3988                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3989            }
3990            _ => {
3991                return Err(KclError::refactor(format!(
3992                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3993                )));
3994            }
3995        };
3996
3997        let seg1_object = self
3998            .scene_graph
3999            .objects
4000            .get(seg1_id.0)
4001            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
4002        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
4003            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
4004        };
4005        let seg1_ast = match seg1_segment {
4006            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
4007                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
4008            }
4009            _ => {
4010                return Err(KclError::refactor(format!(
4011                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4012                )));
4013            }
4014        };
4015
4016        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4017        let (sketch_block_ref, _) = self.mutate_ast(
4018            new_ast,
4019            sketch_id,
4020            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4021        )?;
4022        Ok(sketch_block_ref)
4023    }
4024
4025    async fn add_symmetric(
4026        &mut self,
4027        sketch: ObjectId,
4028        symmetric: Symmetric,
4029        new_ast: &mut ast::Node<ast::Program>,
4030    ) -> Result<AstNodeRef, KclError> {
4031        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4032            return Err(KclError::refactor(format!(
4033                "Symmetric constraint must have exactly 2 inputs, got {}",
4034                symmetric.input.len()
4035            )));
4036        };
4037        let sketch_id = sketch;
4038
4039        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4040        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4041        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4042
4043        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4044        let (sketch_block_ref, _) = self.mutate_ast(
4045            new_ast,
4046            sketch_id,
4047            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4048        )?;
4049        Ok(sketch_block_ref)
4050    }
4051
4052    async fn add_midpoint(
4053        &mut self,
4054        sketch: ObjectId,
4055        midpoint: Midpoint,
4056        new_ast: &mut ast::Node<ast::Program>,
4057    ) -> Result<AstNodeRef, KclError> {
4058        let sketch_id = sketch;
4059        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4060
4061        let segment_object = self
4062            .scene_graph
4063            .objects
4064            .get(midpoint.segment.0)
4065            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4066        let ObjectKind::Segment {
4067            segment: midpoint_segment,
4068        } = &segment_object.kind
4069        else {
4070            return Err(KclError::refactor(format!(
4071                "Object must be a segment, but it was {}",
4072                segment_object.kind.human_friendly_kind_with_article()
4073            )));
4074        };
4075        let segment_ast = match midpoint_segment {
4076            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4077            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4078            _ => {
4079                return Err(KclError::refactor(format!(
4080                    "Midpoint target must be a line or arc segment but it was {}",
4081                    midpoint_segment.human_friendly_kind_with_article()
4082                )));
4083            }
4084        };
4085
4086        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4087        let (sketch_block_ref, _) = self.mutate_ast(
4088            new_ast,
4089            sketch_id,
4090            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4091        )?;
4092        Ok(sketch_block_ref)
4093    }
4094
4095    async fn add_equal_radius(
4096        &mut self,
4097        sketch: ObjectId,
4098        equal_radius: EqualRadius,
4099        new_ast: &mut ast::Node<ast::Program>,
4100    ) -> Result<AstNodeRef, KclError> {
4101        if equal_radius.input.len() < 2 {
4102            return Err(KclError::refactor(format!(
4103                "equalRadius constraint must have at least 2 segments, got {}",
4104                equal_radius.input.len()
4105            )));
4106        }
4107
4108        let sketch_id = sketch;
4109        let input_asts = equal_radius
4110            .input
4111            .iter()
4112            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4113            .collect::<Result<Vec<_>, _>>()?;
4114
4115        let equal_radius_ast = create_equal_radius_ast(input_asts);
4116        let (sketch_block_ref, _) = self.mutate_ast(
4117            new_ast,
4118            sketch_id,
4119            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4120        )?;
4121        Ok(sketch_block_ref)
4122    }
4123
4124    async fn add_radius(
4125        &mut self,
4126        sketch: ObjectId,
4127        radius: Radius,
4128        new_ast: &mut ast::Node<ast::Program>,
4129    ) -> Result<AstNodeRef, KclError> {
4130        let params = ArcSizeConstraintParams {
4131            points: vec![radius.arc],
4132            function_name: RADIUS_FN,
4133            value: radius.radius.value,
4134            units: radius.radius.units,
4135            label_position: radius.label_position,
4136            constraint_type_name: "Radius",
4137        };
4138        self.add_arc_size_constraint(sketch, params, new_ast).await
4139    }
4140
4141    async fn add_diameter(
4142        &mut self,
4143        sketch: ObjectId,
4144        diameter: Diameter,
4145        new_ast: &mut ast::Node<ast::Program>,
4146    ) -> Result<AstNodeRef, KclError> {
4147        let params = ArcSizeConstraintParams {
4148            points: vec![diameter.arc],
4149            function_name: DIAMETER_FN,
4150            value: diameter.diameter.value,
4151            units: diameter.diameter.units,
4152            label_position: diameter.label_position,
4153            constraint_type_name: "Diameter",
4154        };
4155        self.add_arc_size_constraint(sketch, params, new_ast).await
4156    }
4157
4158    async fn add_fixed_constraints(
4159        &mut self,
4160        sketch: ObjectId,
4161        points: Vec<FixedPoint>,
4162        new_ast: &mut ast::Node<ast::Program>,
4163    ) -> Result<AstNodeRef, KclError> {
4164        let mut sketch_block_ref = None;
4165
4166        for fixed_point in points {
4167            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4168            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4169                .map_err(|err| KclError::refactor(err.to_string()))?;
4170
4171            let (sketch_ref, _) = self.mutate_ast(
4172                new_ast,
4173                sketch,
4174                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4175            )?;
4176            sketch_block_ref = Some(sketch_ref);
4177        }
4178
4179        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4180    }
4181
4182    async fn add_arc_size_constraint(
4183        &mut self,
4184        sketch: ObjectId,
4185        params: ArcSizeConstraintParams,
4186        new_ast: &mut ast::Node<ast::Program>,
4187    ) -> Result<AstNodeRef, KclError> {
4188        let sketch_id = sketch;
4189
4190        // Constraint must have exactly 1 argument (arc segment)
4191        if params.points.len() != 1 {
4192            return Err(KclError::refactor(format!(
4193                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4194                params.constraint_type_name,
4195                params.points.len()
4196            )));
4197        }
4198
4199        let arc_id = params.points[0];
4200        let arc_object = self
4201            .scene_graph
4202            .objects
4203            .get(arc_id.0)
4204            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4205        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4206            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4207        };
4208        let ref_type = match arc_segment {
4209            Segment::Arc(_) => ARC_VARIABLE,
4210            Segment::Circle(_) => CIRCLE_VARIABLE,
4211            _ => {
4212                return Err(KclError::refactor(format!(
4213                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4214                    params.constraint_type_name
4215                )));
4216            }
4217        };
4218        // Reference the arc/circle segment directly
4219        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4220        let arguments = match &params.label_position {
4221            Some(label_position) => vec![ast::LabeledArg {
4222                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4223                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4224            }],
4225            None => Default::default(),
4226        };
4227
4228        // Create the function call.
4229        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4230            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4231            unlabeled: Some(arc_ast),
4232            arguments,
4233            digest: None,
4234            non_code_meta: Default::default(),
4235        })));
4236        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4237            left: call_ast,
4238            operator: ast::BinaryOperator::Eq,
4239            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4240                value: ast::LiteralValue::Number {
4241                    value: params.value,
4242                    suffix: params.units,
4243                },
4244                raw: format_number_literal(params.value, params.units, None)
4245                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4246                digest: None,
4247            }))),
4248            digest: None,
4249        })));
4250
4251        // Add the line to the AST of the sketch block.
4252        let (sketch_block_ref, _) = self.mutate_ast(
4253            new_ast,
4254            sketch_id,
4255            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4256        )?;
4257        Ok(sketch_block_ref)
4258    }
4259
4260    async fn add_horizontal_distance(
4261        &mut self,
4262        sketch: ObjectId,
4263        distance: Distance,
4264        new_ast: &mut ast::Node<ast::Program>,
4265    ) -> Result<AstNodeRef, KclError> {
4266        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4267    }
4268
4269    async fn add_vertical_distance(
4270        &mut self,
4271        sketch: ObjectId,
4272        distance: Distance,
4273        new_ast: &mut ast::Node<ast::Program>,
4274    ) -> Result<AstNodeRef, KclError> {
4275        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4276    }
4277
4278    async fn add_horizontal(
4279        &mut self,
4280        sketch: ObjectId,
4281        horizontal: Horizontal,
4282        new_ast: &mut ast::Node<ast::Program>,
4283    ) -> Result<AstNodeRef, KclError> {
4284        let sketch_id = sketch;
4285
4286        // Map the runtime objects back to variable names.
4287        let first_arg_ast = match horizontal {
4288            Horizontal::Line { line } => {
4289                let line_object = self
4290                    .scene_graph
4291                    .objects
4292                    .get(line.0)
4293                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4294                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4295                    let kind = line_object.kind.human_friendly_kind_with_article();
4296                    return Err(KclError::refactor(format!(
4297                        "This constraint only works on Segments, but you selected {kind}"
4298                    )));
4299                };
4300                let Segment::Line(_) = line_segment else {
4301                    return Err(KclError::refactor(format!(
4302                        "Only lines can be made horizontal, but you selected {}",
4303                        line_segment.human_friendly_kind_with_article(),
4304                    )));
4305                };
4306                self.line_id_to_ast_reference(line, new_ast)?
4307            }
4308            Horizontal::Points { points } => {
4309                let point_asts = points
4310                    .iter()
4311                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4312                    .collect::<Result<Vec<_>, _>>()?;
4313                ast::ArrayExpression::new(point_asts).into()
4314            }
4315        };
4316        // Create the horizontal() call using shared helper.
4317        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4318
4319        // Add the line to the AST of the sketch block.
4320        let (sketch_block_ref, _) = self.mutate_ast(
4321            new_ast,
4322            sketch_id,
4323            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4324        )?;
4325        Ok(sketch_block_ref)
4326    }
4327
4328    async fn add_lines_equal_length(
4329        &mut self,
4330        sketch: ObjectId,
4331        lines_equal_length: LinesEqualLength,
4332        new_ast: &mut ast::Node<ast::Program>,
4333    ) -> Result<AstNodeRef, KclError> {
4334        if lines_equal_length.lines.len() < 2 {
4335            return Err(KclError::refactor(format!(
4336                "Lines equal length constraint must have at least 2 lines, got {}",
4337                lines_equal_length.lines.len()
4338            )));
4339        };
4340
4341        let sketch_id = sketch;
4342
4343        // Map the runtime objects back to variable names.
4344        let line_asts = lines_equal_length
4345            .lines
4346            .iter()
4347            .map(|line_id| {
4348                let line_object = self
4349                    .scene_graph
4350                    .objects
4351                    .get(line_id.0)
4352                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4353                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4354                    let kind = line_object.kind.human_friendly_kind_with_article();
4355                    return Err(KclError::refactor(format!(
4356                        "This constraint only works on Segments, but you selected {kind}"
4357                    )));
4358                };
4359                let Segment::Line(_) = line_segment else {
4360                    let kind = line_segment.human_friendly_kind_with_article();
4361                    return Err(KclError::refactor(format!(
4362                        "Only lines can be made equal length, but you selected {kind}"
4363                    )));
4364                };
4365
4366                self.line_id_to_ast_reference(*line_id, new_ast)
4367            })
4368            .collect::<Result<Vec<_>, _>>()?;
4369
4370        // Create the equalLength() call using shared helper.
4371        let equal_length_ast = create_equal_length_ast(line_asts);
4372
4373        // Add the constraint to the AST of the sketch block.
4374        let (sketch_block_ref, _) = self.mutate_ast(
4375            new_ast,
4376            sketch_id,
4377            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4378        )?;
4379        Ok(sketch_block_ref)
4380    }
4381
4382    fn equal_radius_segment_id_to_ast_reference(
4383        &mut self,
4384        segment_id: ObjectId,
4385        new_ast: &mut ast::Node<ast::Program>,
4386    ) -> Result<ast::Expr, KclError> {
4387        let segment_object = self
4388            .scene_graph
4389            .objects
4390            .get(segment_id.0)
4391            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4392        let ObjectKind::Segment { segment } = &segment_object.kind else {
4393            return Err(KclError::refactor(format!(
4394                "Object is not a segment, it was {}",
4395                segment_object.kind.human_friendly_kind_with_article()
4396            )));
4397        };
4398
4399        let ref_type = match segment {
4400            Segment::Arc(_) => ARC_VARIABLE,
4401            Segment::Circle(_) => CIRCLE_VARIABLE,
4402            _ => {
4403                return Err(KclError::refactor(format!(
4404                    "equalRadius supports only arc/circle segments, got {}",
4405                    segment.human_friendly_kind_with_article()
4406                )));
4407            }
4408        };
4409
4410        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4411    }
4412
4413    fn symmetric_input_id_to_ast_reference(
4414        &mut self,
4415        segment_id: ObjectId,
4416        new_ast: &mut ast::Node<ast::Program>,
4417    ) -> Result<ast::Expr, KclError> {
4418        let segment_object = self
4419            .scene_graph
4420            .objects
4421            .get(segment_id.0)
4422            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4423        let ObjectKind::Segment { segment } = &segment_object.kind else {
4424            return Err(KclError::refactor(format!(
4425                "Object is not a segment, it was {}",
4426                segment_object.kind.human_friendly_kind_with_article()
4427            )));
4428        };
4429
4430        match segment {
4431            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4432            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4433            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4434            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4435            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4436                "Symmetric does not yet support control point splines".to_owned(),
4437            )),
4438        }
4439    }
4440
4441    fn symmetric_axis_id_to_ast_reference(
4442        &mut self,
4443        segment_id: ObjectId,
4444        new_ast: &mut ast::Node<ast::Program>,
4445    ) -> Result<ast::Expr, KclError> {
4446        let segment_object = self
4447            .scene_graph
4448            .objects
4449            .get(segment_id.0)
4450            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4451        let ObjectKind::Segment { segment } = &segment_object.kind else {
4452            return Err(KclError::refactor(format!(
4453                "Object is not a segment, it was {}",
4454                segment_object.kind.human_friendly_kind_with_article()
4455            )));
4456        };
4457        match segment {
4458            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4459            _ => Err(KclError::refactor(format!(
4460                "Symmetric axis must be a line, got {}",
4461                segment.human_friendly_kind_with_article()
4462            ))),
4463        }
4464    }
4465
4466    async fn add_parallel(
4467        &mut self,
4468        sketch: ObjectId,
4469        parallel: Parallel,
4470        new_ast: &mut ast::Node<ast::Program>,
4471    ) -> Result<AstNodeRef, KclError> {
4472        if parallel.lines.len() < 2 {
4473            return Err(KclError::refactor(format!(
4474                "Parallel constraint must have at least 2 lines, got {}",
4475                parallel.lines.len()
4476            )));
4477        };
4478
4479        let sketch_id = sketch;
4480
4481        let line_asts = parallel
4482            .lines
4483            .iter()
4484            .map(|line_id| {
4485                let line_object = self
4486                    .scene_graph
4487                    .objects
4488                    .get(line_id.0)
4489                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4490                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4491                    let kind = line_object.kind.human_friendly_kind_with_article();
4492                    return Err(KclError::refactor(format!(
4493                        "This constraint only works on Segments, but you selected {kind}"
4494                    )));
4495                };
4496                let Segment::Line(_) = line_segment else {
4497                    let kind = line_segment.human_friendly_kind_with_article();
4498                    return Err(KclError::refactor(format!(
4499                        "Only lines can be made parallel, but you selected {kind}"
4500                    )));
4501                };
4502
4503                self.line_id_to_ast_reference(*line_id, new_ast)
4504            })
4505            .collect::<Result<Vec<_>, _>>()?;
4506
4507        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4508            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4509            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4510                ast::ArrayExpression {
4511                    elements: line_asts,
4512                    digest: None,
4513                    non_code_meta: Default::default(),
4514                },
4515            )))),
4516            arguments: Default::default(),
4517            digest: None,
4518            non_code_meta: Default::default(),
4519        })));
4520
4521        let (sketch_block_ref, _) = self.mutate_ast(
4522            new_ast,
4523            sketch_id,
4524            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4525        )?;
4526        Ok(sketch_block_ref)
4527    }
4528
4529    async fn add_perpendicular(
4530        &mut self,
4531        sketch: ObjectId,
4532        perpendicular: Perpendicular,
4533        new_ast: &mut ast::Node<ast::Program>,
4534    ) -> Result<AstNodeRef, KclError> {
4535        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4536            .await
4537    }
4538
4539    async fn add_lines_at_angle_constraint(
4540        &mut self,
4541        sketch: ObjectId,
4542        angle_kind: LinesAtAngleKind,
4543        lines: Vec<ObjectId>,
4544        new_ast: &mut ast::Node<ast::Program>,
4545    ) -> Result<AstNodeRef, KclError> {
4546        let &[line0_id, line1_id] = lines.as_slice() else {
4547            return Err(KclError::refactor(format!(
4548                "{} constraint must have exactly 2 lines, got {}",
4549                angle_kind.to_function_name(),
4550                lines.len()
4551            )));
4552        };
4553
4554        let sketch_id = sketch;
4555
4556        // Map the runtime objects back to variable names.
4557        let line0_object = self
4558            .scene_graph
4559            .objects
4560            .get(line0_id.0)
4561            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4562        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4563            let kind = line0_object.kind.human_friendly_kind_with_article();
4564            return Err(KclError::refactor(format!(
4565                "This constraint only works on Segments, but you selected {kind}"
4566            )));
4567        };
4568        let Segment::Line(_) = line0_segment else {
4569            return Err(KclError::refactor(format!(
4570                "Only lines can be made {}, but you selected {}",
4571                angle_kind.to_function_name(),
4572                line0_segment.human_friendly_kind_with_article(),
4573            )));
4574        };
4575        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4576
4577        let line1_object = self
4578            .scene_graph
4579            .objects
4580            .get(line1_id.0)
4581            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4582        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4583            let kind = line1_object.kind.human_friendly_kind_with_article();
4584            return Err(KclError::refactor(format!(
4585                "This constraint only works on Segments, but you selected {kind}"
4586            )));
4587        };
4588        let Segment::Line(_) = line1_segment else {
4589            return Err(KclError::refactor(format!(
4590                "Only lines can be made {}, but you selected {}",
4591                angle_kind.to_function_name(),
4592                line1_segment.human_friendly_kind_with_article(),
4593            )));
4594        };
4595        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4596
4597        // Create the parallel() or perpendicular() call.
4598        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4599            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4600            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4601                ast::ArrayExpression {
4602                    elements: vec![line0_ast, line1_ast],
4603                    digest: None,
4604                    non_code_meta: Default::default(),
4605                },
4606            )))),
4607            arguments: Default::default(),
4608            digest: None,
4609            non_code_meta: Default::default(),
4610        })));
4611
4612        // Add the constraint to the AST of the sketch block.
4613        let (sketch_block_ref, _) = self.mutate_ast(
4614            new_ast,
4615            sketch_id,
4616            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4617        )?;
4618        Ok(sketch_block_ref)
4619    }
4620
4621    async fn add_vertical(
4622        &mut self,
4623        sketch: ObjectId,
4624        vertical: Vertical,
4625        new_ast: &mut ast::Node<ast::Program>,
4626    ) -> Result<AstNodeRef, KclError> {
4627        let sketch_id = sketch;
4628
4629        let first_arg_ast = match vertical {
4630            Vertical::Line { line } => {
4631                // Map the runtime objects back to variable names.
4632                let line_object = self
4633                    .scene_graph
4634                    .objects
4635                    .get(line.0)
4636                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4637                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4638                    let kind = line_object.kind.human_friendly_kind_with_article();
4639                    return Err(KclError::refactor(format!(
4640                        "This constraint only works on Segments, but you selected {kind}"
4641                    )));
4642                };
4643                let Segment::Line(_) = line_segment else {
4644                    return Err(KclError::refactor(format!(
4645                        "Only lines can be made vertical, but you selected {}",
4646                        line_segment.human_friendly_kind_with_article()
4647                    )));
4648                };
4649                self.line_id_to_ast_reference(line, new_ast)?
4650            }
4651            Vertical::Points { points } => {
4652                let point_asts = points
4653                    .iter()
4654                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4655                    .collect::<Result<Vec<_>, _>>()?;
4656                ast::ArrayExpression::new(point_asts).into()
4657            }
4658        };
4659        // Create the vertical() call using shared helper.
4660        let vertical_ast = create_vertical_ast(first_arg_ast);
4661
4662        // Add the line to the AST of the sketch block.
4663        let (sketch_block_ref, _) = self.mutate_ast(
4664            new_ast,
4665            sketch_id,
4666            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4667        )?;
4668        Ok(sketch_block_ref)
4669    }
4670
4671    async fn execute_after_add_constraint(
4672        &mut self,
4673        ctx: &ExecutorContext,
4674        sketch_id: ObjectId,
4675        sketch_block_ref: AstNodeRef,
4676        new_ast: &mut ast::Node<ast::Program>,
4677    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4678        // Convert to string source to create real source ranges.
4679        let new_source = source_from_ast(new_ast);
4680        // Parse the new KCL source.
4681        let new_program = parse_frontend_mutation_source(
4682            &new_source,
4683            "Error parsing KCL source after adding constraint",
4684            "No AST produced after adding constraint",
4685        )?;
4686        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4687            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4688                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4689            )))
4690        })?;
4691
4692        // Truncate after the sketch block for mock execution.
4693        // Use a clone so we don't mutate new_program yet
4694        let mut truncated_program = new_program.clone();
4695        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4696            .map_err(KclErrorWithOutputs::no_outputs)?;
4697
4698        // Execute - if this fails, we haven't modified self yet, so state is safe
4699        let outcome = ctx
4700            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4701            .await?;
4702
4703        let new_object_ids = {
4704            // Extract the constraint ID from the execution outcome using source_range_to_object
4705            let constraint_id = outcome
4706                .source_range_to_object
4707                .get(&constraint_node_ref.range)
4708                .copied()
4709                .ok_or_else(|| {
4710                    KclErrorWithOutputs::from_error_outcome(
4711                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4712                        outcome.clone(),
4713                    )
4714                })?;
4715            vec![constraint_id]
4716        };
4717
4718        // Only now, after all operations succeeded, update self.program.
4719        // This ensures state is only modified if everything succeeds.
4720        self.program = new_program;
4721
4722        // Uses MockConfig::default() which has freedom_analysis: true
4723        let outcome = self.update_state_after_exec(outcome, true);
4724
4725        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4726        let scene_graph_delta = SceneGraphDelta {
4727            new_graph: self.scene_graph_for_ui(),
4728            invalidates_ids: false,
4729            new_objects: new_object_ids,
4730            exec_outcome: outcome,
4731        };
4732        Ok((src_delta, scene_graph_delta))
4733    }
4734
4735    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4736        let commit_failure = || {
4737            KclErrorWithOutputs::from_error_outcome(
4738                KclError::refactor(format!("Could not update KCL after {operation}.")),
4739                outcome.clone(),
4740            )
4741        };
4742
4743        let default_length_unit = self.default_length_unit();
4744        let mut settled_ast = self.program.ast.clone();
4745        let mut committed_solver_value = false;
4746        for (var_range, node_path, value) in &outcome.var_solutions {
4747            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4748                return Err(commit_failure());
4749            };
4750            let new_value = match &lookup {
4751                Some(current_literal) => {
4752                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4753                        continue;
4754                    }
4755                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4756                }
4757                None => {
4758                    // Bare `var` with no initial literal to compare against;
4759                    // always commit, using the module's default length unit as
4760                    // an explicit suffix so the written value carries units.
4761                    Number {
4762                        value: number_value_in_default_length_units(*value, default_length_unit),
4763                        units: default_length_unit.into(),
4764                    }
4765                }
4766            };
4767            committed_solver_value = true;
4768            let source_ref = SourceRef::Simple {
4769                range: *var_range,
4770                node_path: node_path.clone(),
4771            };
4772            mutate_ast_node_by_source_ref(
4773                &mut settled_ast,
4774                &source_ref,
4775                AstMutateCommand::EditVarInitialValue { value: new_value },
4776            )
4777            .map_err(|_| commit_failure())?;
4778        }
4779
4780        if !committed_solver_value {
4781            return Ok(SourceDelta {
4782                text: self.program.original_file_contents.clone(),
4783            });
4784        }
4785
4786        let settled_source = source_from_ast(&settled_ast);
4787        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4788        if !errors.is_empty() {
4789            return Err(commit_failure());
4790        }
4791        let Some(settled_program) = settled_program else {
4792            return Err(commit_failure());
4793        };
4794
4795        self.program = settled_program;
4796
4797        Ok(SourceDelta { text: settled_source })
4798    }
4799
4800    // Find constraints that reference the given segments.
4801    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4802        if segment_ids_set.contains(&segment_id) {
4803            return true;
4804        }
4805
4806        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4807            return false;
4808        };
4809        let ObjectKind::Segment { segment } = &segment_object.kind else {
4810            return false;
4811        };
4812        let Segment::Point(point) = segment else {
4813            return false;
4814        };
4815
4816        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4817    }
4818
4819    fn remaining_constraint_segments(
4820        &self,
4821        segments: &[ConstraintSegment],
4822        segment_ids_set: &AhashIndexSet<ObjectId>,
4823    ) -> Vec<ConstraintSegment> {
4824        segments
4825            .iter()
4826            .copied()
4827            .filter(|segment| match segment {
4828                ConstraintSegment::Origin(_) => true,
4829                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4830            })
4831            .collect()
4832    }
4833
4834    fn find_referenced_constraints(
4835        &self,
4836        sketch_id: ObjectId,
4837        segment_ids_set: &AhashIndexSet<ObjectId>,
4838    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4839        // Look up the sketch.
4840        let sketch_object = self
4841            .scene_graph
4842            .objects
4843            .get(sketch_id.0)
4844            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4845        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4846            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4847        };
4848        let segment_or_owner_matches = |segment_id: ObjectId| {
4849            if segment_ids_set.contains(&segment_id) {
4850                return true;
4851            }
4852            let segment_object = self.scene_graph.objects.get(segment_id.0);
4853            if let Some(obj) = segment_object
4854                && let ObjectKind::Segment { segment } = &obj.kind
4855            {
4856                match segment {
4857                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4858                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4859                    _ => false,
4860                }
4861            } else {
4862                false
4863            }
4864        };
4865        let mut constraint_ids_set = AhashIndexSet::default();
4866        for constraint_id in &sketch.constraints {
4867            let constraint_object = self
4868                .scene_graph
4869                .objects
4870                .get(constraint_id.0)
4871                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4872            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4873                return Err(KclError::refactor(format!(
4874                    "Object is not a constraint, it is {}",
4875                    constraint_object.kind.human_friendly_kind_with_article()
4876                )));
4877            };
4878            let depends_on_segment = match constraint {
4879                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4880                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4881                Constraint::Fixed(fixed) => fixed
4882                    .points
4883                    .iter()
4884                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4885                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4886                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4887                Constraint::EqualRadius(equal_radius) => {
4888                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4889                }
4890                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4891                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4892                Constraint::Horizontal(h) => match h {
4893                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4894                    Horizontal::Points { points } => points.iter().any(|point| match point {
4895                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4896                        ConstraintSegment::Origin(_) => false,
4897                    }),
4898                },
4899                Constraint::Vertical(v) => match v {
4900                    Vertical::Line { line } => segment_or_owner_matches(*line),
4901                    Vertical::Points { points } => points.iter().any(|point| match point {
4902                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4903                        ConstraintSegment::Origin(_) => false,
4904                    }),
4905                },
4906                Constraint::LinesEqualLength(lines_equal_length) => {
4907                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4908                }
4909                Constraint::Midpoint(midpoint) => {
4910                    segment_or_owner_matches(midpoint.segment)
4911                        || matches!(
4912                            midpoint.point,
4913                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4914                        )
4915                }
4916                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4917                Constraint::Perpendicular(perpendicular) => {
4918                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4919                }
4920                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4921                Constraint::Symmetric(symmetric) => {
4922                    segment_or_owner_matches(symmetric.axis)
4923                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4924                }
4925                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4926            };
4927            if depends_on_segment {
4928                constraint_ids_set.insert(*constraint_id);
4929            }
4930        }
4931        Ok(constraint_ids_set)
4932    }
4933
4934    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4935        let mut outcome = outcome;
4936        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4937        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4938
4939        if freedom_analysis_ran {
4940            // When freedom analysis ran, replace the cache entirely with new values
4941            // Don't merge with old values since IDs might have changed
4942            self.point_freedom_cache.clear();
4943            for new_obj in &new_objects {
4944                if let ObjectKind::Segment {
4945                    segment: kcl_api::Segment::Point(point),
4946                } = &new_obj.kind
4947                {
4948                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4949                }
4950            }
4951            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4952            // Objects are already correct from the analysis, just use them as-is
4953            self.scene_graph.objects = new_objects;
4954        } else {
4955            // When freedom analysis didn't run, preserve old values and merge
4956            // Before replacing objects, extract and store freedom values from old objects
4957            for old_obj in &self.scene_graph.objects {
4958                if let ObjectKind::Segment {
4959                    segment: kcl_api::Segment::Point(point),
4960                } = &old_obj.kind
4961                {
4962                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4963                }
4964            }
4965
4966            // Update objects, preserving stored freedom values when new is Free (might be default)
4967            let mut updated_objects = Vec::with_capacity(new_objects.len());
4968            for new_obj in new_objects {
4969                let mut obj = new_obj;
4970                if let ObjectKind::Segment {
4971                    segment: kcl_api::Segment::Point(point),
4972                } = &mut obj.kind
4973                {
4974                    let new_freedom = point.freedom;
4975                    // When freedom_analysis=false, new values are defaults (Free).
4976                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4977                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4978                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4979                    // when there are actually unsatisfied constraints.
4980                    match new_freedom {
4981                        Freedom::Free => {
4982                            match self.point_freedom_cache.get(&obj.id).copied() {
4983                                Some(Freedom::Conflict) => {
4984                                    // Don't preserve Conflict - conflicts are transient
4985                                    // Keep it as Free
4986                                }
4987                                Some(Freedom::Fixed) => {
4988                                    // Preserve Fixed cached value
4989                                    point.freedom = Freedom::Fixed;
4990                                }
4991                                Some(Freedom::Free) => {
4992                                    // If stored is also Free, keep Free (no change needed)
4993                                }
4994                                None => {
4995                                    // If no cached value, keep Free (default)
4996                                }
4997                            }
4998                        }
4999                        Freedom::Fixed => {
5000                            // Use new value (already set)
5001                        }
5002                        Freedom::Conflict => {
5003                            // Use new value (already set)
5004                        }
5005                    }
5006                    // Store the new freedom value (even if it's Free, so we know it was set)
5007                    self.point_freedom_cache.insert(obj.id, point.freedom);
5008                }
5009                updated_objects.push(obj);
5010            }
5011
5012            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5013            self.scene_graph.objects = updated_objects;
5014        }
5015        outcome
5016    }
5017
5018    fn mutate_ast(
5019        &mut self,
5020        ast: &mut ast::Node<ast::Program>,
5021        object_id: ObjectId,
5022        command: AstMutateCommand,
5023    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5024        let sketch_object = self
5025            .scene_graph
5026            .objects
5027            .get(object_id.0)
5028            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5029        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5030    }
5031
5032    fn mutate_constraint_label_position(
5033        &mut self,
5034        ast: &mut ast::Node<ast::Program>,
5035        constraint_id: ObjectId,
5036        label_position: Point2d<Number>,
5037    ) -> Result<(), KclError> {
5038        let object = self
5039            .scene_graph
5040            .objects
5041            .get(constraint_id.0)
5042            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5043        if !matches!(
5044            &object.kind,
5045            ObjectKind::Constraint {
5046                constraint: Constraint::Distance(_)
5047                    | Constraint::HorizontalDistance(_)
5048                    | Constraint::VerticalDistance(_)
5049                    | Constraint::Radius(_)
5050                    | Constraint::Diameter(_)
5051                    | Constraint::Angle(_),
5052            }
5053        ) {
5054            return Err(KclError::refactor(format!(
5055                "Object does not support labelPosition: {constraint_id:?}"
5056            )));
5057        }
5058
5059        let label_position = to_ast_point2d_number(&label_position)
5060            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5061        self.mutate_ast(
5062            ast,
5063            constraint_id,
5064            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5065        )?;
5066        Ok(())
5067    }
5068}
5069
5070fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5071    // Look up existing sketch.
5072    let sketch_object = scene_graph
5073        .objects
5074        .get(sketch_id.0)
5075        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5076    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5077        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5078    };
5079    expect_single_node_ref(sketch_object)
5080}
5081
5082fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5083    match &object.source {
5084        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5085            range: *range,
5086            node_path: node_path.clone(),
5087        }),
5088        SourceRef::BackTrace { ranges } => {
5089            let [range] = ranges.as_slice() else {
5090                return Err(KclError::refactor(format!(
5091                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5092                    ranges.len()
5093                )));
5094            };
5095            Ok(AstNodeRef {
5096                range: range.0,
5097                node_path: range.1.clone(),
5098            })
5099        }
5100    }
5101}
5102
5103/// This is a deprecated fall-back implementation. Prefer
5104/// [`only_sketch_block()`] to avoid reliance on source ranges.
5105fn only_sketch_block_from_range(
5106    ast: &mut ast::Node<ast::Program>,
5107    sketch_block_range: SourceRange,
5108    edit_kind: ChangeKind,
5109) -> Result<(), KclError> {
5110    let r1 = sketch_block_range;
5111    let matches_range = |r2: SourceRange| -> bool {
5112        // We may have added items to the sketch block, so the end may not be an
5113        // exact match.
5114        match edit_kind {
5115            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5116            // For edit, we don't know whether it grew or shrank.
5117            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5118            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5119            // No edit should be an exact match.
5120            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5121        }
5122    };
5123    let mut found = false;
5124    for item in ast.body.iter_mut() {
5125        match item {
5126            ast::BodyItem::ImportStatement(_) => {}
5127            ast::BodyItem::ExpressionStatement(node) => {
5128                if matches_range(SourceRange::from(&*node))
5129                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5130                {
5131                    sketch_block.is_being_edited = true;
5132                    found = true;
5133                    break;
5134                }
5135            }
5136            ast::BodyItem::VariableDeclaration(node) => {
5137                if matches_range(SourceRange::from(&node.declaration.init))
5138                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5139                {
5140                    sketch_block.is_being_edited = true;
5141                    found = true;
5142                    break;
5143                }
5144            }
5145            ast::BodyItem::TypeDeclaration(_) => {}
5146            ast::BodyItem::ReturnStatement(node) => {
5147                if matches_range(SourceRange::from(&node.argument))
5148                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5149                {
5150                    sketch_block.is_being_edited = true;
5151                    found = true;
5152                    break;
5153                }
5154            }
5155        }
5156    }
5157    if !found {
5158        return Err(KclError::refactor(format!(
5159            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5160        )));
5161    }
5162
5163    Ok(())
5164}
5165
5166fn only_sketch_block(
5167    ast: &mut ast::Node<ast::Program>,
5168    sketch_block_ref: &AstNodeRef,
5169    edit_kind: ChangeKind,
5170) -> Result<(), KclError> {
5171    let Some(target_node_path) = &sketch_block_ref.node_path else {
5172        #[cfg(target_arch = "wasm32")]
5173        web_sys::console::warn_1(
5174            &format!(
5175                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5176                sketch_block_ref
5177            )
5178            .into(),
5179        );
5180        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5181    };
5182    struct MarkSketchBlockBeingEdited<'a> {
5183        target_node_path: &'a ast::NodePath,
5184    }
5185
5186    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5187        type Break = ();
5188        type Continue = ();
5189
5190        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5191            if let NodeMut::SketchBlock(sketch_block) = node
5192                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5193            {
5194                sketch_block.is_being_edited = true;
5195                return TraversalReturn::new_break(());
5196            }
5197            TraversalReturn::new_continue(())
5198        }
5199
5200        fn finish(&mut self, _node: NodeMut<'_>) {}
5201    }
5202
5203    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5204    let found = dfs_mut(ast, &mut marker).is_break();
5205    if !found {
5206        return Err(KclError::refactor(format!(
5207            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5208        )));
5209    }
5210
5211    Ok(())
5212}
5213
5214fn sketch_on_ast_expr(
5215    ast: &mut ast::Node<ast::Program>,
5216    scene_graph: &SceneGraph,
5217    solid_references: &HashMap<Uuid, SolidAstReference>,
5218    on: &Plane,
5219) -> Result<ast::Expr, KclError> {
5220    match on {
5221        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5222        Plane::Object(object_id) => {
5223            let on_object = scene_graph
5224                .objects
5225                .get(object_id.0)
5226                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5227            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5228                return Ok(face_expr);
5229            }
5230            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5231        }
5232        Plane::PrimitiveFace(face) => {
5233            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5234                KclError::refactor(format!(
5235                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5236                    face.solid_id
5237                ))
5238            })?;
5239            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5240            Ok(create_face_of_ast(solid_expr, face_id_expr))
5241        }
5242    }
5243}
5244
5245fn solid_references_from_variables(
5246    ast: &ast::Node<ast::Program>,
5247    variables: &IndexMap<String, KclValueView>,
5248) -> HashMap<Uuid, SolidAstReference> {
5249    let mut references = HashMap::new();
5250
5251    // In-place operations such as shell reuse their input solid's engine ID.
5252    // Later variables overwrite earlier references so mutations target the result.
5253    for item in &ast.body {
5254        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5255            continue;
5256        };
5257        let name = &declaration.declaration.id.name;
5258        let Some(value) = variables.get(name) else {
5259            continue;
5260        };
5261
5262        match value {
5263            KclValueView::Solid { value } => {
5264                references.insert(
5265                    value.id,
5266                    SolidAstReference {
5267                        variable_name: name.clone(),
5268                        output_index: None,
5269                    },
5270                );
5271            }
5272            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5273                for (output_index, entry) in value.iter().enumerate() {
5274                    if let KclValueView::Solid { value } = entry {
5275                        references.insert(
5276                            value.id,
5277                            SolidAstReference {
5278                                variable_name: name.clone(),
5279                                output_index: Some(output_index),
5280                            },
5281                        );
5282                    }
5283                }
5284            }
5285            _ => {}
5286        }
5287    }
5288
5289    references
5290}
5291
5292fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5293    let reference = solid_references.get(&solid_id)?;
5294    let solid_expr = ast_name_expr(reference.variable_name.clone());
5295    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5296}
5297
5298fn sketch_face_of_scene_object_ast_expr(
5299    ast: &mut ast::Node<ast::Program>,
5300    on_object: &kcl_api::Object,
5301) -> Result<Option<ast::Expr>, KclError> {
5302    match &on_object.kind {
5303        ObjectKind::Wall(wall) => {
5304            let solid_ref = get_or_insert_ast_reference(
5305                ast,
5306                &source_ref_from_source_ref_range(&wall.source.solid),
5307                "solid",
5308                None,
5309            )?;
5310            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5311                return Err(KclError::refactor(format!(
5312                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5313                    on_object.artifact_id
5314                )));
5315            };
5316            let solid_expr = indexed_solid_expr_for_sweep_output(
5317                ast_name_expr(solid_name_expr.name.name.clone()),
5318                wall.solid_output_index,
5319            );
5320            let sweep_ref = get_or_insert_ast_reference(
5321                ast,
5322                &source_ref_from_source_ref_range(&wall.source.sweep),
5323                "solid",
5324                None,
5325            )?;
5326            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5327                return Err(KclError::refactor(format!(
5328                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5329                    on_object.artifact_id
5330                )));
5331            };
5332            let sweep_name = sweep_name_expr.name.name.clone();
5333            let segment_ref = get_or_insert_ast_reference(
5334                ast,
5335                &source_ref_from_source_ref_range(&wall.source.segment),
5336                LINE_VARIABLE,
5337                None,
5338            )?;
5339
5340            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5341                wall.source
5342                    .path
5343                    .as_ref()
5344                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5345            }) {
5346                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5347                    return Err(KclError::refactor(format!(
5348                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5349                        on_object.artifact_id
5350                    )));
5351                };
5352                create_member_expression(
5353                    create_member_expression(ast_name_expr(region_name), "tags"),
5354                    &segment_name_expr.name.name,
5355                )
5356            } else {
5357                segment_ref
5358            };
5359
5360            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5361        }
5362        ObjectKind::Cap(cap) => {
5363            let solid_ref =
5364                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5365            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5366                return Err(KclError::refactor(format!(
5367                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5368                    on_object.artifact_id
5369                )));
5370            };
5371            let solid_expr = indexed_solid_expr_for_sweep_output(
5372                ast_name_expr(solid_name_expr.name.name.clone()),
5373                cap.solid_output_index,
5374            );
5375            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5376            let face_expr = match cap.kind {
5377                kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5378                kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5379            };
5380
5381            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5382        }
5383        _ => Ok(None),
5384    }
5385}
5386
5387fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5388    match solid_output_index {
5389        Some(output_index) => create_index_expression(solid_expr, output_index),
5390        None => solid_expr,
5391    }
5392}
5393
5394fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5395    SourceRef::Simple {
5396        range: source.range,
5397        node_path: source.node_path.clone(),
5398    }
5399}
5400
5401fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5402    let source_ref = source_ref_from_source_ref_range(path_source);
5403    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5404    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5405        return None;
5406    };
5407    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5408        return None;
5409    };
5410    if region_call.callee.name.name != "region" {
5411        return None;
5412    }
5413    Some(candidate)
5414}
5415
5416fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5417    let mut current_id = source_id;
5418    let mut current_composite = None;
5419    let mut visited = HashSet::new();
5420
5421    while visited.insert(current_id) {
5422        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5423
5424        let Some(composite_id) = next_composite_id else {
5425            break;
5426        };
5427        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5428            break;
5429        };
5430
5431        current_id = composite.id;
5432        current_composite = Some(composite);
5433
5434        if !composite.consumed {
5435            break;
5436        }
5437    }
5438
5439    current_composite.map(|composite| &composite.code_ref)
5440}
5441
5442fn downstream_composite_id_for_solid_source(
5443    artifact_graph: &ArtifactGraph,
5444    source_id: ArtifactId,
5445) -> Option<ArtifactId> {
5446    // Source is a path, find its solid.
5447    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5448        && let Some(composite_id) = path.composite_solid_id
5449        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5450        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5451    {
5452        return Some(composite_id);
5453    }
5454
5455    // Source is a sweep, find its path -> then find the solid
5456    for artifact in artifact_graph.values() {
5457        if let Artifact::Path(path) = artifact
5458            && path.sweep_id == Some(source_id)
5459            && let Some(composite_id) = path.composite_solid_id
5460            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5461            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5462        {
5463            return Some(composite_id);
5464        }
5465    }
5466
5467    // Source is a solid, find its downstream solid.
5468    artifact_graph.values().find_map(|artifact| {
5469        let Artifact::CompositeSolid(composite) = artifact else {
5470            return None;
5471        };
5472        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5473    })
5474}
5475
5476fn composite_contains_path_input(
5477    solid_ids: &[ArtifactId],
5478    tool_ids: &[ArtifactId],
5479    path_id: ArtifactId,
5480    solid2d_id: Option<ArtifactId>,
5481) -> bool {
5482    composite_contains_input(solid_ids, tool_ids, path_id)
5483        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5484}
5485
5486fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5487    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5488}
5489
5490fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5491    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5492    SourceRefRange {
5493        range: code_ref.range,
5494        node_path,
5495    }
5496}
5497
5498fn solid_output_index_for_sweep(
5499    artifact_graph: &ArtifactGraph,
5500    sweep_id: ArtifactId,
5501    sweep_code_ref: &CodeRef,
5502) -> Option<usize> {
5503    // Constituent sweeps are implementation details of one composite body,
5504    // even when cloning gives them the same CodeRef as the composite root.
5505    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5506        return None;
5507    }
5508
5509    let sibling_sweeps = artifact_graph
5510        .values()
5511        .filter_map(|artifact| match artifact {
5512            Artifact::Sweep(sweep)
5513                if sweep.code_ref.range == sweep_code_ref.range
5514                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5515            {
5516                Some(sweep)
5517            }
5518            _ => None,
5519        })
5520        .collect::<Vec<_>>();
5521
5522    if sibling_sweeps.len() <= 1 {
5523        return None;
5524    }
5525
5526    sibling_sweeps
5527        .iter()
5528        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5529}
5530
5531fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5532    let mut existing_artifact_ids = scene_objects
5533        .iter()
5534        .map(|object| object.artifact_id)
5535        .collect::<HashSet<_>>();
5536
5537    for artifact in artifact_graph.values() {
5538        match artifact {
5539            Artifact::Wall(wall) => {
5540                if existing_artifact_ids.contains(&wall.id) {
5541                    continue;
5542                }
5543
5544                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5545                    Artifact::Segment(segment) => Some(segment),
5546                    _ => None,
5547                }) else {
5548                    continue;
5549                };
5550                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5551                    Artifact::Sweep(sweep) => Some(sweep),
5552                    _ => None,
5553                }) else {
5554                    continue;
5555                };
5556                let source_segment = segment
5557                    .original_seg_id
5558                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5559                    .and_then(|artifact| match artifact {
5560                        Artifact::Segment(segment) => Some(segment),
5561                        _ => None,
5562                    })
5563                    .unwrap_or(segment);
5564                let solid_code_ref =
5565                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5566                let path_code_ref = artifact_graph
5567                    .get(&segment.path_id)
5568                    .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5569                    .and_then(|artifact| match artifact {
5570                        Artifact::Path(path) => Some(&path.code_ref),
5571                        _ => None,
5572                    });
5573                let source = WallSource {
5574                    solid: code_ref_source_ref_range(solid_code_ref),
5575                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5576                    path: path_code_ref.map(code_ref_source_ref_range),
5577                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5578                };
5579                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5580                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5581                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5582                    .flatten();
5583                let object_source = source_ref_from_source_ref_range(&source.solid);
5584                let id = ObjectId(scene_objects.len());
5585                scene_objects.push(kcl_api::Object {
5586                    id,
5587                    kind: ObjectKind::Wall(kcl_api::Wall {
5588                        id,
5589                        source,
5590                        solid_output_index,
5591                    }),
5592                    label: Default::default(),
5593                    comments: Default::default(),
5594                    artifact_id: wall.id,
5595                    source: object_source,
5596                });
5597                existing_artifact_ids.insert(wall.id);
5598            }
5599            Artifact::Cap(cap) => {
5600                if existing_artifact_ids.contains(&cap.id) {
5601                    continue;
5602                }
5603
5604                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5605                    Artifact::Sweep(sweep) => Some(sweep),
5606                    _ => None,
5607                }) else {
5608                    continue;
5609                };
5610                let id = ObjectId(scene_objects.len());
5611                let kind = match cap.sub_type {
5612                    CapSubType::Start => kcl_api::CapKind::Start,
5613                    CapSubType::End => kcl_api::CapKind::End,
5614                };
5615                let solid_code_ref =
5616                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5617                let source = CapSource {
5618                    solid: code_ref_source_ref_range(solid_code_ref),
5619                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5620                };
5621                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5622                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5623                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5624                    .flatten();
5625                let object_source = source_ref_from_source_ref_range(&source.solid);
5626                scene_objects.push(kcl_api::Object {
5627                    id,
5628                    kind: ObjectKind::Cap(kcl_api::Cap {
5629                        id,
5630                        kind,
5631                        source,
5632                        solid_output_index,
5633                    }),
5634                    label: Default::default(),
5635                    comments: Default::default(),
5636                    artifact_id: cap.id,
5637                    source: object_source,
5638                });
5639                existing_artifact_ids.insert(cap.id);
5640            }
5641            _ => {}
5642        }
5643    }
5644}
5645
5646fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5647    use crate::engine::PlaneName;
5648
5649    match name {
5650        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5651        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5652        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5653        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5654        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5655        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5656    }
5657}
5658
5659fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5660    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5661        ast::UnaryOperator::Neg,
5662        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5663    )))
5664}
5665
5666fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5667    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5668        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5669        unlabeled: Some(solid_expr),
5670        arguments: vec![ast::LabeledArg {
5671            label: Some(ast::Identifier::new("face")),
5672            arg: face_expr,
5673        }],
5674        digest: None,
5675        non_code_meta: Default::default(),
5676    })))
5677}
5678
5679fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5680    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5681        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5682        unlabeled: Some(solid_expr),
5683        arguments: vec![ast::LabeledArg {
5684            label: Some(ast::Identifier::new("index")),
5685            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5686                ast::NumericLiteral {
5687                    value: index as f64,
5688                    suffix: NumericSuffix::None,
5689                    raw: index.to_string(),
5690                    digest: None,
5691                },
5692            )))),
5693        }],
5694        digest: None,
5695        non_code_meta: Default::default(),
5696    })))
5697}
5698
5699fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5700    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5701        return None;
5702    };
5703    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5704        return None;
5705    };
5706    if !matches!(
5707        sweep_call.callee.name.name.as_str(),
5708        "extrude" | "revolve" | "sweep" | "loft"
5709    ) {
5710        return None;
5711    }
5712    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5713        return None;
5714    };
5715    let candidate = region_name_expr.name.name.clone();
5716    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5717        return None;
5718    };
5719    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5720        return None;
5721    };
5722    if region_call.callee.name.name != "region" {
5723        return None;
5724    }
5725    Some(candidate)
5726}
5727
5728/// Return the AST expression referencing the variable at the given source ref.
5729/// If no such variable exists, insert a new variable declaration with the given
5730/// prefix.
5731///
5732/// This may return a complex expression referencing properties of the variable
5733/// (e.g., `line1.start`).
5734fn get_or_insert_ast_reference(
5735    ast: &mut ast::Node<ast::Program>,
5736    source_ref: &SourceRef,
5737    prefix: &str,
5738    property: Option<&str>,
5739) -> Result<ast::Expr, KclError> {
5740    let command = AstMutateCommand::AddVariableDeclaration {
5741        prefix: prefix.to_owned(),
5742    };
5743    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5744        Ok((_, ret)) => ret,
5745        Err(err) => {
5746            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5747                AstMutateCommandReturn::Name(var_name)
5748            } else {
5749                return Err(err);
5750            }
5751        }
5752    };
5753    let AstMutateCommandReturn::Name(var_name) = ret else {
5754        return Err(KclError::refactor(
5755            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5756        ));
5757    };
5758    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5759    let Some(property) = property else {
5760        // No property; just return the variable name.
5761        return Ok(var_expr);
5762    };
5763
5764    Ok(create_member_expression(var_expr, property))
5765}
5766
5767fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5768    let source_range = match source_ref {
5769        SourceRef::Simple { range, .. } => *range,
5770        SourceRef::BackTrace { ranges } => {
5771            let [range] = ranges.as_slice() else {
5772                return None;
5773            };
5774            range.0
5775        }
5776    };
5777    ast.body.iter().find_map(|item| {
5778        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5779            return None;
5780        };
5781        let init_range = SourceRange::from(&var_decl.declaration.init);
5782        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5783            && init_range.start() <= source_range.start()
5784            && source_range.end() <= init_range.end();
5785        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5786            && init_range != source_range
5787            && source_is_inside_init
5788        {
5789            return None;
5790        }
5791        source_is_inside_init.then(|| var_decl.name().to_owned())
5792    })
5793}
5794
5795fn mutate_ast_node_by_source_ref(
5796    ast: &mut ast::Node<ast::Program>,
5797    source_ref: &SourceRef,
5798    command: AstMutateCommand,
5799) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5800    let (source_range, node_path) = match source_ref {
5801        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5802        SourceRef::BackTrace { ranges } => {
5803            let [range] = ranges.as_slice() else {
5804                return Err(KclError::refactor(format!(
5805                    "Expected single source ref, got {}; ranges={ranges:#?}",
5806                    ranges.len(),
5807                )));
5808            };
5809            (range.0, range.1.clone())
5810        }
5811    };
5812    let mut context = AstMutateContext {
5813        source_range,
5814        node_path,
5815        command,
5816        defined_names_stack: Default::default(),
5817    };
5818    let control = dfs_mut(ast, &mut context);
5819    match control {
5820        ControlFlow::Continue(_) => Err(KclError::refactor(
5821            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5822        )),
5823        ControlFlow::Break(break_value) => break_value,
5824    }
5825}
5826
5827#[derive(Debug)]
5828struct AstMutateContext {
5829    source_range: SourceRange,
5830    node_path: Option<ast::NodePath>,
5831    command: AstMutateCommand,
5832    defined_names_stack: Vec<HashSet<String>>,
5833}
5834
5835#[derive(Debug)]
5836#[allow(clippy::large_enum_variant)]
5837enum AstMutateCommand {
5838    /// Add an expression statement to the sketch block.
5839    AddSketchBlockExprStmt {
5840        expr: ast::Expr,
5841    },
5842    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5843    AddSketchBlockVarDecl {
5844        prefix: String,
5845        expr: ast::Expr,
5846    },
5847    AddVariableDeclaration {
5848        prefix: String,
5849    },
5850    EditPoint {
5851        at: ast::Expr,
5852    },
5853    EditLine {
5854        start: ast::Expr,
5855        end: ast::Expr,
5856        construction: Option<bool>,
5857    },
5858    EditArc {
5859        start: ast::Expr,
5860        end: ast::Expr,
5861        center: ast::Expr,
5862        direction: Option<ArcDirection>,
5863        construction: Option<bool>,
5864    },
5865    EditCircle {
5866        start: ast::Expr,
5867        center: ast::Expr,
5868        construction: Option<bool>,
5869    },
5870    EditControlPointSpline {
5871        points: ast::Expr,
5872        construction: Option<bool>,
5873    },
5874    EditConstraintValue {
5875        value: ast::BinaryPart,
5876    },
5877    EditAngleConstraint {
5878        call: ast::BinaryPart,
5879        value: ast::BinaryPart,
5880    },
5881    EditDistanceConstraint {
5882        call: ast::BinaryPart,
5883        value: ast::BinaryPart,
5884    },
5885    EditDistanceConstraintLabelPosition {
5886        label_position: ast::Expr,
5887    },
5888    EditCallUnlabeled {
5889        arg: ast::Expr,
5890    },
5891    EditVarInitialValue {
5892        value: Number,
5893    },
5894    DeleteNode,
5895}
5896
5897impl AstMutateCommand {
5898    fn needs_defined_names_stack(&self) -> bool {
5899        matches!(
5900            self,
5901            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5902        )
5903    }
5904}
5905
5906#[derive(Debug)]
5907enum AstMutateCommandReturn {
5908    None,
5909    Name(String),
5910}
5911
5912#[derive(Debug, Clone)]
5913struct AstNodeRef {
5914    range: SourceRange,
5915    node_path: Option<ast::NodePath>,
5916}
5917
5918impl<T> From<&ast::Node<T>> for AstNodeRef {
5919    fn from(value: &ast::Node<T>) -> Self {
5920        AstNodeRef {
5921            range: value.into(),
5922            node_path: value.node_path.clone(),
5923        }
5924    }
5925}
5926
5927impl From<&ast::BodyItem> for AstNodeRef {
5928    fn from(value: &ast::BodyItem) -> Self {
5929        match value {
5930            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5931                range: node.into(),
5932                node_path: node.node_path.clone(),
5933            },
5934            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5935                range: node.into(),
5936                node_path: node.node_path.clone(),
5937            },
5938            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5939                range: node.into(),
5940                node_path: node.node_path.clone(),
5941            },
5942            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5943                range: node.into(),
5944                node_path: node.node_path.clone(),
5945            },
5946            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5947                range: node.into(),
5948                node_path: node.node_path.clone(),
5949            },
5950        }
5951    }
5952}
5953
5954impl From<&ast::Expr> for AstNodeRef {
5955    fn from(value: &ast::Expr) -> Self {
5956        AstNodeRef {
5957            range: SourceRange::from(value),
5958            node_path: value.node_path().cloned(),
5959        }
5960    }
5961}
5962
5963impl From<&AstMutateContext> for AstNodeRef {
5964    fn from(value: &AstMutateContext) -> Self {
5965        AstNodeRef {
5966            range: value.source_range,
5967            node_path: value.node_path.clone(),
5968        }
5969    }
5970}
5971
5972impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5973    type Error = crate::walk::AstNodeError;
5974
5975    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5976        Ok(AstNodeRef {
5977            range: SourceRange::try_from(value)?,
5978            node_path: value.try_into()?,
5979        })
5980    }
5981}
5982
5983impl From<AstNodeRef> for SourceRange {
5984    fn from(value: AstNodeRef) -> Self {
5985        value.range
5986    }
5987}
5988
5989impl Visitor for AstMutateContext {
5990    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5991    type Continue = ();
5992
5993    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5994        filter_and_process(self, node)
5995    }
5996
5997    fn finish(&mut self, node: NodeMut<'_>) {
5998        match &node {
5999            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
6000                self.defined_names_stack.pop();
6001            }
6002            _ => {}
6003        }
6004    }
6005}
6006
6007fn filter_and_process(
6008    ctx: &mut AstMutateContext,
6009    node: NodeMut,
6010) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
6011    let Ok(node_range) = SourceRange::try_from(&node) else {
6012        // Nodes that can't be converted to a range aren't interesting.
6013        return TraversalReturn::new_continue(());
6014    };
6015    // If we're adding a variable declaration, we need to look at variable
6016    // declaration expressions to see if it already has a variable, before
6017    // continuing. The variable declaration's source range won't match the
6018    // target; its init expression will.
6019    if let NodeMut::VariableDeclaration(var_decl) = &node {
6020        let expr_range = SourceRange::from(&var_decl.declaration.init);
6021        let expr_node_path = var_decl.declaration.init.node_path();
6022        if source_ref_matches(ctx, expr_range, expr_node_path) {
6023            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024                // We found the variable declaration expression. It doesn't need
6025                // to be added.
6026                return TraversalReturn::new_break(Ok((
6027                    AstNodeRef::from(&**var_decl),
6028                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6029                )));
6030            }
6031            if let AstMutateCommand::DeleteNode = &ctx.command {
6032                // We found the variable declaration. Delete the variable along
6033                // with the segment.
6034                return TraversalReturn {
6035                    mutate_body_item: MutateBodyItem::Delete,
6036                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037                };
6038            }
6039        }
6040    }
6041    // Similar thing with expression statement. We need to look at the
6042    // expression inside it.
6043    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6044        let expr_range = SourceRange::from(&expr_stmt.expression);
6045        let expr_node_path = expr_stmt.expression.node_path();
6046        if source_ref_matches(ctx, expr_range, expr_node_path) {
6047            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6048                // We found the node wrapped in an expression statement. Process
6049                // the statement.
6050                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6051                    return TraversalReturn::new_continue(());
6052                };
6053                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6054            }
6055            if let AstMutateCommand::DeleteNode = &ctx.command {
6056                // We found the node wrapped in an expression statement. Delete
6057                // the whole statement.
6058                return TraversalReturn {
6059                    mutate_body_item: MutateBodyItem::Delete,
6060                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6061                };
6062            }
6063        }
6064    }
6065
6066    if ctx.command.needs_defined_names_stack() {
6067        if let NodeMut::Program(program) = &node {
6068            ctx.defined_names_stack.push(find_defined_names(*program));
6069        } else if let NodeMut::SketchBlock(block) = &node {
6070            ctx.defined_names_stack.push(find_defined_names(&block.body));
6071        }
6072    }
6073
6074    // Make sure the node matches the source ref.
6075    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6076    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6077        return TraversalReturn::new_continue(());
6078    }
6079    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6080        return TraversalReturn::new_continue(());
6081    };
6082    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6083}
6084
6085fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6086    match &ctx.node_path {
6087        Some(target) => Some(target) == node_path,
6088        None => node_range == ctx.source_range,
6089    }
6090}
6091
6092fn is_angle_constraint_call_name(name: &str) -> bool {
6093    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6094}
6095
6096fn is_distance_constraint_call_name(name: &str) -> bool {
6097    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6098}
6099
6100fn is_constraint_call_name(name: &str) -> bool {
6101    matches!(
6102        name,
6103        DISTANCE_FN
6104            | HORIZONTAL_DISTANCE_FN
6105            | VERTICAL_DISTANCE_FN
6106            | RADIUS_FN
6107            | DIAMETER_FN
6108            | ANGLE_FN
6109            | ANGLE_DIMENSION_FN
6110    )
6111}
6112
6113fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6114    if let ast::BinaryPart::CallExpressionKw(call) = part
6115        && is_constraint_call_name(call.callee.name.name.as_str())
6116    {
6117        Some(call)
6118    } else {
6119        None
6120    }
6121}
6122
6123fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6124    match &ctx.command {
6125        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6126            if let NodeMut::SketchBlock(sketch_block) = node {
6127                sketch_block
6128                    .body
6129                    .items
6130                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6131                        inner: ast::ExpressionStatement {
6132                            expression: expr.clone(),
6133                            digest: None,
6134                        },
6135                        start: Default::default(),
6136                        end: Default::default(),
6137                        module_id: Default::default(),
6138                        node_path: None,
6139                        outer_attrs: Default::default(),
6140                        pre_comments: Default::default(),
6141                        comment_start: Default::default(),
6142                    }));
6143                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6144            }
6145        }
6146        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6147            if let NodeMut::SketchBlock(sketch_block) = node {
6148                let empty_defined_names = HashSet::new();
6149                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6150                let Ok(name) = next_free_name(prefix, defined_names) else {
6151                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152                };
6153                sketch_block
6154                    .body
6155                    .items
6156                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6157                        ast::VariableDeclaration::new(
6158                            ast::VariableDeclarator::new(&name, expr.clone()),
6159                            ast::ItemVisibility::Default,
6160                            ast::VariableKind::Const,
6161                        ),
6162                    ))));
6163                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6164            }
6165        }
6166        AstMutateCommand::AddVariableDeclaration { prefix } => {
6167            if let NodeMut::VariableDeclaration(inner) = node {
6168                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6169            }
6170            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6171                let empty_defined_names = HashSet::new();
6172                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6173                let Ok(name) = next_free_name(prefix, defined_names) else {
6174                    // TODO: Return an error instead?
6175                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6176                };
6177                let mutate_node =
6178                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6179                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6180                        ast::ItemVisibility::Default,
6181                        ast::VariableKind::Const,
6182                    ))));
6183                return TraversalReturn {
6184                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6185                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6186                };
6187            }
6188        }
6189        AstMutateCommand::EditPoint { at } => {
6190            if let NodeMut::CallExpressionKw(call) = node {
6191                if call.callee.name.name != POINT_FN {
6192                    return TraversalReturn::new_continue(());
6193                }
6194                // Update the arguments.
6195                for labeled_arg in &mut call.arguments {
6196                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6197                        labeled_arg.arg = at.clone();
6198                    }
6199                }
6200                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6201            }
6202        }
6203        AstMutateCommand::EditLine {
6204            start,
6205            end,
6206            construction,
6207        } => {
6208            if let NodeMut::CallExpressionKw(call) = node {
6209                if call.callee.name.name != LINE_FN {
6210                    return TraversalReturn::new_continue(());
6211                }
6212                // Update the arguments.
6213                for labeled_arg in &mut call.arguments {
6214                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6215                        labeled_arg.arg = start.clone();
6216                    }
6217                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6218                        labeled_arg.arg = end.clone();
6219                    }
6220                }
6221                // Handle construction kwarg
6222                if let Some(construction_value) = construction {
6223                    let construction_exists = call
6224                        .arguments
6225                        .iter()
6226                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6227                    if *construction_value {
6228                        // Add or update construction=true
6229                        if construction_exists {
6230                            // Update existing construction kwarg
6231                            for labeled_arg in &mut call.arguments {
6232                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6233                                    labeled_arg.arg =
6234                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6235                                            value: ast::LiteralValue::Bool(true),
6236                                            raw: "true".to_string(),
6237                                            digest: None,
6238                                        })));
6239                                }
6240                            }
6241                        } else {
6242                            // Add new construction kwarg
6243                            call.arguments.push(ast::LabeledArg {
6244                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6245                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6246                                    value: ast::LiteralValue::Bool(true),
6247                                    raw: "true".to_string(),
6248                                    digest: None,
6249                                }))),
6250                            });
6251                        }
6252                    } else {
6253                        // Remove construction kwarg if it exists
6254                        call.arguments
6255                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6256                    }
6257                }
6258                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6259            }
6260        }
6261        AstMutateCommand::EditArc {
6262            start,
6263            end,
6264            center,
6265            direction,
6266            construction,
6267        } => {
6268            if let NodeMut::CallExpressionKw(call) = node {
6269                if call.callee.name.name != ARC_FN {
6270                    return TraversalReturn::new_continue(());
6271                }
6272                // Update the arguments.
6273                for labeled_arg in &mut call.arguments {
6274                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6275                        labeled_arg.arg = start.clone();
6276                    }
6277                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6278                        labeled_arg.arg = end.clone();
6279                    }
6280                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6281                        labeled_arg.arg = center.clone();
6282                    }
6283                }
6284                // Handle direction kwarg
6285                if let Some(direction_value) = direction {
6286                    let direction_exists = call
6287                        .arguments
6288                        .iter()
6289                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6290                    if direction_value.is_clockwise() {
6291                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6292                        if direction_exists {
6293                            // Update existing direction kwarg
6294                            for labeled_arg in &mut call.arguments {
6295                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6296                                    labeled_arg.arg = direction_ast.clone();
6297                                }
6298                            }
6299                        } else {
6300                            // Add new direction kwarg
6301                            call.arguments.push(ast::LabeledArg {
6302                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6303                                arg: direction_ast,
6304                            });
6305                        }
6306                    } else {
6307                        // Remove direction kwarg if it exists since
6308                        // counterclockwise is the default
6309                        call.arguments
6310                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6311                    }
6312                }
6313                // Handle construction kwarg
6314                if let Some(construction_value) = construction {
6315                    let construction_exists = call
6316                        .arguments
6317                        .iter()
6318                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6319                    if *construction_value {
6320                        // Add or update construction=true
6321                        if construction_exists {
6322                            // Update existing construction kwarg
6323                            for labeled_arg in &mut call.arguments {
6324                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6325                                    labeled_arg.arg =
6326                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6327                                            value: ast::LiteralValue::Bool(true),
6328                                            raw: "true".to_string(),
6329                                            digest: None,
6330                                        })));
6331                                }
6332                            }
6333                        } else {
6334                            // Add new construction kwarg
6335                            call.arguments.push(ast::LabeledArg {
6336                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6337                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6338                                    value: ast::LiteralValue::Bool(true),
6339                                    raw: "true".to_string(),
6340                                    digest: None,
6341                                }))),
6342                            });
6343                        }
6344                    } else {
6345                        // Remove construction kwarg if it exists
6346                        call.arguments
6347                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6348                    }
6349                }
6350                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6351            }
6352        }
6353        AstMutateCommand::EditCircle {
6354            start,
6355            center,
6356            construction,
6357        } => {
6358            if let NodeMut::CallExpressionKw(call) = node {
6359                if call.callee.name.name != CIRCLE_FN {
6360                    return TraversalReturn::new_continue(());
6361                }
6362                // Update the arguments.
6363                for labeled_arg in &mut call.arguments {
6364                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6365                        labeled_arg.arg = start.clone();
6366                    }
6367                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6368                        labeled_arg.arg = center.clone();
6369                    }
6370                }
6371                // Handle construction kwarg
6372                if let Some(construction_value) = construction {
6373                    let construction_exists = call
6374                        .arguments
6375                        .iter()
6376                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6377                    if *construction_value {
6378                        if construction_exists {
6379                            // Update existing construction kwarg
6380                            for labeled_arg in &mut call.arguments {
6381                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6382                                    labeled_arg.arg =
6383                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6384                                            value: ast::LiteralValue::Bool(true),
6385                                            raw: "true".to_string(),
6386                                            digest: None,
6387                                        })));
6388                                }
6389                            }
6390                        } else {
6391                            // Add new construction kwarg
6392                            call.arguments.push(ast::LabeledArg {
6393                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6394                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6395                                    value: ast::LiteralValue::Bool(true),
6396                                    raw: "true".to_string(),
6397                                    digest: None,
6398                                }))),
6399                            });
6400                        }
6401                    } else {
6402                        // Remove construction kwarg if it exists
6403                        call.arguments
6404                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6405                    }
6406                }
6407                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6408            }
6409        }
6410        AstMutateCommand::EditControlPointSpline { points, construction } => {
6411            if let NodeMut::CallExpressionKw(call) = node {
6412                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6413                    return TraversalReturn::new_continue(());
6414                }
6415                for labeled_arg in &mut call.arguments {
6416                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6417                    {
6418                        labeled_arg.arg = points.clone();
6419                    }
6420                }
6421                // Handle construction kwarg
6422                if let Some(construction_value) = construction {
6423                    let construction_exists = call
6424                        .arguments
6425                        .iter()
6426                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6427                    if *construction_value {
6428                        if construction_exists {
6429                            for labeled_arg in &mut call.arguments {
6430                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6431                                    labeled_arg.arg =
6432                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6433                                            value: ast::LiteralValue::Bool(true),
6434                                            raw: "true".to_string(),
6435                                            digest: None,
6436                                        })));
6437                                }
6438                            }
6439                        } else {
6440                            call.arguments.push(ast::LabeledArg {
6441                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6442                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6443                                    value: ast::LiteralValue::Bool(true),
6444                                    raw: "true".to_string(),
6445                                    digest: None,
6446                                }))),
6447                            });
6448                        }
6449                    } else {
6450                        call.arguments
6451                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6452                    }
6453                }
6454                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6455            }
6456        }
6457        AstMutateCommand::EditConstraintValue { value } => {
6458            if let NodeMut::BinaryExpression(binary_expr) = node {
6459                let left_is_constraint = matches!(
6460                    &binary_expr.left,
6461                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6462                );
6463                if left_is_constraint {
6464                    binary_expr.right = value.clone();
6465                } else {
6466                    binary_expr.left = value.clone();
6467                }
6468
6469                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6470            }
6471        }
6472        AstMutateCommand::EditAngleConstraint { call, value } => {
6473            if let NodeMut::BinaryExpression(binary_expr) = node {
6474                let left_is_angle = matches!(
6475                    &binary_expr.left,
6476                    ast::BinaryPart::CallExpressionKw(existing_call)
6477                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6478                );
6479                let right_is_angle = matches!(
6480                    &binary_expr.right,
6481                    ast::BinaryPart::CallExpressionKw(existing_call)
6482                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6483                );
6484
6485                match (left_is_angle, right_is_angle) {
6486                    (true, _) => {
6487                        binary_expr.left = call.clone();
6488                        binary_expr.right = value.clone();
6489                    }
6490                    (false, true) => {
6491                        binary_expr.left = value.clone();
6492                        binary_expr.right = call.clone();
6493                    }
6494                    (false, false) => return TraversalReturn::new_continue(()),
6495                }
6496
6497                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6498            }
6499        }
6500        AstMutateCommand::EditDistanceConstraint { call, value } => {
6501            if let NodeMut::BinaryExpression(binary_expr) = node {
6502                let left_is_distance = matches!(
6503                    &binary_expr.left,
6504                    ast::BinaryPart::CallExpressionKw(existing_call)
6505                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6506                );
6507                let right_is_distance = matches!(
6508                    &binary_expr.right,
6509                    ast::BinaryPart::CallExpressionKw(existing_call)
6510                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6511                );
6512
6513                match (left_is_distance, right_is_distance) {
6514                    (true, _) => {
6515                        binary_expr.left = call.clone();
6516                        binary_expr.right = value.clone();
6517                    }
6518                    (false, true) => {
6519                        binary_expr.left = value.clone();
6520                        binary_expr.right = call.clone();
6521                    }
6522                    (false, false) => return TraversalReturn::new_continue(()),
6523                }
6524
6525                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6526            }
6527        }
6528        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6529            if let NodeMut::BinaryExpression(binary_expr) = node {
6530                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6531                    call
6532                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6533                    call
6534                } else {
6535                    return TraversalReturn::new_continue(());
6536                };
6537
6538                if let Some(label_arg) = call
6539                    .arguments
6540                    .iter_mut()
6541                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6542                {
6543                    label_arg.arg = label_position.clone();
6544                } else {
6545                    call.arguments.push(ast::LabeledArg {
6546                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6547                        arg: label_position.clone(),
6548                    });
6549                }
6550
6551                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6552            }
6553        }
6554        AstMutateCommand::EditCallUnlabeled { arg } => {
6555            if let NodeMut::CallExpressionKw(call) = node {
6556                call.unlabeled = Some(arg.clone());
6557                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6558            }
6559        }
6560        AstMutateCommand::EditVarInitialValue { value } => {
6561            // We target the SketchVar itself (matched by NodePath) rather than
6562            // the inner NumericLiteral so we can also write back into vars that
6563            // were declared without an initial value (e.g. bare `var`).
6564            if let NodeMut::SketchVar(sketch_var) = node {
6565                let Ok(literal) = to_source_number(*value) else {
6566                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6567                        "Could not convert number to AST literal: {:?}",
6568                        *value
6569                    ))));
6570                };
6571                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6572                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6573            }
6574        }
6575        AstMutateCommand::DeleteNode => {
6576            return TraversalReturn {
6577                mutate_body_item: MutateBodyItem::Delete,
6578                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6579            };
6580        }
6581    }
6582    TraversalReturn::new_continue(())
6583}
6584
6585struct FindSketchBlockSourceRange {
6586    /// The source range of the sketch block before mutation.
6587    target_before_mutation: SourceRange,
6588    /// The source range of the sketch block's last body item after mutation. We
6589    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6590    /// shared reference.
6591    found: Cell<Option<AstNodeRef>>,
6592}
6593
6594impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6595    type Error = crate::front::Error;
6596
6597    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6598        let Ok(node_range) = SourceRange::try_from(&node) else {
6599            return Ok(true);
6600        };
6601
6602        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6603            if node_range.module_id() == self.target_before_mutation.module_id()
6604                && node_range.start() == self.target_before_mutation.start()
6605                // End shouldn't match since we added something.
6606                && node_range.end() >= self.target_before_mutation.end()
6607            {
6608                self.found.set(sketch_block.body.items.last().map(|item| match item {
6609                    // For declarations like `circle1 = circle(...)`, use
6610                    // the init expression range so lookup in source_range_to_object
6611                    // matches the segment source range.
6612                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6613                    _ => AstNodeRef::from(item),
6614                }));
6615                return Ok(false);
6616            } else {
6617                // We found a different sketch block. No need to descend into
6618                // its children since sketch blocks cannot be nested.
6619                return Ok(true);
6620            }
6621        }
6622
6623        for child in node.children().iter() {
6624            if !child.visit(*self)? {
6625                return Ok(false);
6626            }
6627        }
6628
6629        Ok(true)
6630    }
6631}
6632
6633struct FindSketchBlockByNodePath {
6634    /// The Node Path of the sketch block before mutation.
6635    target_node_path: ast::NodePath,
6636    /// The ref of the sketch block's last body item after mutation. We need to
6637    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6638    /// reference.
6639    found: Cell<Option<AstNodeRef>>,
6640}
6641
6642impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6643    type Error = crate::front::Error;
6644
6645    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6646        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6647            return Ok(true);
6648        };
6649
6650        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6651            if let Some(node_path) = node_path
6652                && node_path == self.target_node_path
6653            {
6654                self.found.set(sketch_block.body.items.last().map(|item| match item {
6655                    // For declarations like `circle1 = circle(...)`, use
6656                    // the init expression range so lookup in source_range_to_object
6657                    // matches the segment source range.
6658                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6659                    _ => AstNodeRef::from(item),
6660                }));
6661
6662                return Ok(false);
6663            } else {
6664                // We found a different sketch block. No need to descend into
6665                // its children since sketch blocks cannot be nested.
6666                return Ok(true);
6667            }
6668        }
6669
6670        for child in node.children().iter() {
6671            if !child.visit(*self)? {
6672                return Ok(false);
6673            }
6674        }
6675
6676        Ok(true)
6677    }
6678}
6679
6680/// After adding an item to a sketch block, find the sketch block, and get the
6681/// source range of the added item. We assume that the added item is the last
6682/// item in the sketch block and that the sketch block's source range has grown,
6683/// but not moved from its starting offset.
6684///
6685/// TODO: Do we need to format *before* mutation in case formatting moves the
6686/// sketch block forward?
6687fn find_sketch_block_added_item(
6688    ast: &ast::Node<ast::Program>,
6689    sketch_block_before_mutation: &AstNodeRef,
6690) -> Result<AstNodeRef, KclError> {
6691    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6692        let find = FindSketchBlockByNodePath {
6693            target_node_path: node_path.clone(),
6694            found: Cell::new(None),
6695        };
6696        let node = crate::walk::Node::from(ast);
6697        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6698        find.found.into_inner().ok_or_else(|| {
6699            KclError::refactor(format!(
6700                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6701            ))
6702        })
6703    } else {
6704        // No NodePath. Fall back to legacy source range.
6705        let find = FindSketchBlockSourceRange {
6706            target_before_mutation: sketch_block_before_mutation.range,
6707            found: Cell::new(None),
6708        };
6709        let node = crate::walk::Node::from(ast);
6710        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6711        find.found.into_inner().ok_or_else(|| KclError::refactor(
6712            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?"),
6713        ))
6714    }
6715}
6716
6717fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6718    let message = error.message().trim();
6719    let message = if message.is_empty() {
6720        "unknown parse error"
6721    } else {
6722        message
6723    };
6724
6725    format!("{prefix}: {message}")
6726}
6727
6728fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6729    let (program, errors) = Program::parse(source).map_err(|err| {
6730        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6731    })?;
6732    if !errors.is_empty() {
6733        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6734            format_compilation_issues(parse_error_prefix, &errors),
6735        )));
6736    }
6737
6738    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6739}
6740
6741fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6742    let Some(first_issue) = issues
6743        .iter()
6744        .find(|issue| issue.severity.is_err())
6745        .or_else(|| issues.first())
6746    else {
6747        return prefix.to_owned();
6748    };
6749
6750    let message = first_issue.message.trim();
6751    let message = if message.is_empty() {
6752        "unknown parse error"
6753    } else {
6754        message
6755    };
6756
6757    if issues.len() > 1 {
6758        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6759    } else {
6760        format!("{prefix}: {message}")
6761    }
6762}
6763
6764fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6765    // TODO: Don't duplicate this from lib.rs Program.
6766    ast.recast_top(&Default::default(), 0)
6767}
6768
6769struct FindNumericLiteral {
6770    target: SourceRange,
6771    found: Cell<Option<ast::NumericLiteral>>,
6772}
6773
6774impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6775    type Error = crate::front::Error;
6776
6777    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6778        let Ok(node_range) = SourceRange::try_from(&node) else {
6779            return Ok(true);
6780        };
6781
6782        if node_range == self.target
6783            && let crate::walk::Node::NumericLiteral(literal) = node
6784        {
6785            self.found.set(Some(literal.inner.clone()));
6786            return Ok(false);
6787        }
6788
6789        for child in node.children().iter() {
6790            if !child.visit(*self)? {
6791                return Ok(false);
6792            }
6793        }
6794
6795        Ok(true)
6796    }
6797}
6798
6799fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6800    let find = FindNumericLiteral {
6801        target,
6802        found: Cell::new(None),
6803    };
6804    let node = crate::walk::Node::from(ast);
6805    node.visit(&find).ok()?;
6806    find.found.into_inner()
6807}
6808
6809struct FindSketchVarInitialByNodePath<'a> {
6810    target: &'a ast::NodePath,
6811    sketch_var_found: Cell<bool>,
6812    initial_literal: Cell<Option<ast::NumericLiteral>>,
6813}
6814
6815impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6816    type Error = crate::front::Error;
6817
6818    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6819        if let crate::walk::Node::SketchVar(sketch_var) = node
6820            && sketch_var.node_path.as_ref() == Some(self.target)
6821        {
6822            self.sketch_var_found.set(true);
6823            if let Some(initial) = &sketch_var.initial {
6824                self.initial_literal.set(Some(initial.inner.clone()));
6825            }
6826            return Ok(false);
6827        }
6828
6829        for child in node.children().iter() {
6830            if !child.visit(*self)? {
6831                return Ok(false);
6832            }
6833        }
6834
6835        Ok(true)
6836    }
6837}
6838
6839/// Locate the source `var` declaration corresponding to a sketch-var solution.
6840///
6841/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6842/// from "target found." The inner [`Option`] is the initial numeric literal of
6843/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6844///
6845/// When `node_path` is `None` (e.g. for older outcomes that predate the
6846/// node-path propagation), this falls back to source-range matching, which
6847/// can break under whitespace shifts elsewhere in the file.
6848fn numeric_literal_at_node_path(
6849    ast: &ast::Node<ast::Program>,
6850    node_path: Option<&ast::NodePath>,
6851    source_range: SourceRange,
6852) -> Option<Option<ast::NumericLiteral>> {
6853    let Some(node_path) = node_path else {
6854        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";
6855        #[cfg(target_arch = "wasm32")]
6856        web_sys::console::warn_1(&message.into());
6857        #[cfg(not(target_arch = "wasm32"))]
6858        eprintln!("WARNING: {message}");
6859        return numeric_literal_at_source_range(ast, source_range).map(Some);
6860    };
6861    let find = FindSketchVarInitialByNodePath {
6862        target: node_path,
6863        sketch_var_found: Cell::new(false),
6864        initial_literal: Cell::new(None),
6865    };
6866    let node = crate::walk::Node::from(ast);
6867    node.visit(&find).ok()?;
6868    if !find.sketch_var_found.get() {
6869        return None;
6870    }
6871    Some(find.initial_literal.into_inner())
6872}
6873
6874fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6875    match suffix {
6876        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6877        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6878        NumericSuffix::M => Some(UnitLength::Meters),
6879        NumericSuffix::Inch => Some(UnitLength::Inches),
6880        NumericSuffix::Ft => Some(UnitLength::Feet),
6881        NumericSuffix::Yd => Some(UnitLength::Yards),
6882        _ => None,
6883    }
6884}
6885
6886fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6887    match suffix_length_unit(number.units) {
6888        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6889        None => number.value,
6890    }
6891}
6892
6893fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6894    match suffix_length_unit(literal.suffix) {
6895        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6896        None => literal.value,
6897    }
6898}
6899
6900fn var_solution_needs_commit(
6901    current_literal: &ast::NumericLiteral,
6902    solved_value: Number,
6903    default_length_unit: UnitLength,
6904) -> bool {
6905    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6906    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6907
6908    (current - solved).abs() > 1e-9
6909}
6910
6911fn preserve_var_solution_literal_style(
6912    current_literal: &ast::NumericLiteral,
6913    solved_value: Number,
6914    default_length_unit: UnitLength,
6915) -> Number {
6916    if current_literal.suffix == NumericSuffix::None {
6917        return Number {
6918            value: number_value_in_default_length_units(solved_value, default_length_unit),
6919            units: NumericSuffix::None,
6920        };
6921    }
6922
6923    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6924        return solved_value;
6925    };
6926
6927    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6928    Number {
6929        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6930        units: current_literal.suffix,
6931    }
6932}
6933
6934pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6935    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6936        inner: ast::ArrayExpression {
6937            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6938            non_code_meta: Default::default(),
6939            digest: None,
6940        },
6941        start: Default::default(),
6942        end: Default::default(),
6943        module_id: Default::default(),
6944        node_path: None,
6945        outer_attrs: Default::default(),
6946        pre_comments: Default::default(),
6947        comment_start: Default::default(),
6948    })))
6949}
6950
6951pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6952    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6953        ast::ArrayExpression {
6954            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6955            digest: None,
6956            non_code_meta: Default::default(),
6957        },
6958    ))))
6959}
6960
6961fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6962    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6963        ast::ArrayExpression {
6964            elements: vec![
6965                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6966                    point.x,
6967                )?)))),
6968                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6969                    point.y,
6970                )?)))),
6971            ],
6972            non_code_meta: Default::default(),
6973            digest: None,
6974        },
6975    ))))
6976}
6977
6978fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6979    match expr {
6980        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6981            inner: ast::Literal::from(to_source_number(*number)?),
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::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6991            inner: ast::SketchVar {
6992                initial: Some(BoxNode::new(ast::Node {
6993                    inner: to_source_number(*number)?,
6994                    start: Default::default(),
6995                    end: Default::default(),
6996                    module_id: Default::default(),
6997                    node_path: None,
6998                    outer_attrs: Default::default(),
6999                    pre_comments: Default::default(),
7000                    comment_start: Default::default(),
7001                })),
7002                digest: None,
7003            },
7004            start: Default::default(),
7005            end: Default::default(),
7006            module_id: Default::default(),
7007            node_path: None,
7008            outer_attrs: Default::default(),
7009            pre_comments: Default::default(),
7010            comment_start: Default::default(),
7011        }))),
7012        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7013    }
7014}
7015
7016fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7017    Ok(ast::NumericLiteral {
7018        value: number.value,
7019        suffix: number.units,
7020        raw: format_number_literal(number.value, number.units, None)?,
7021        digest: None,
7022    })
7023}
7024
7025pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7026    ast::Expr::Name(BoxNode::new(ast_name(name)))
7027}
7028
7029fn ast_name(name: String) -> ast::Node<ast::Name> {
7030    ast::Node {
7031        inner: ast::Name {
7032            name: ast::Node {
7033                inner: ast::Identifier { name, digest: None },
7034                start: Default::default(),
7035                end: Default::default(),
7036                module_id: Default::default(),
7037                node_path: None,
7038                outer_attrs: Default::default(),
7039                pre_comments: Default::default(),
7040                comment_start: Default::default(),
7041            },
7042            path: Vec::new(),
7043            abs_path: false,
7044            digest: None,
7045        },
7046        start: Default::default(),
7047        end: Default::default(),
7048        module_id: Default::default(),
7049        node_path: None,
7050        outer_attrs: Default::default(),
7051        pre_comments: Default::default(),
7052        comment_start: Default::default(),
7053    }
7054}
7055
7056pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7057    ast::Name {
7058        name: ast::Node {
7059            inner: ast::Identifier {
7060                name: name.to_owned(),
7061                digest: None,
7062            },
7063            start: Default::default(),
7064            end: Default::default(),
7065            module_id: Default::default(),
7066            node_path: None,
7067            outer_attrs: Default::default(),
7068            pre_comments: Default::default(),
7069            comment_start: Default::default(),
7070        },
7071        path: Default::default(),
7072        abs_path: false,
7073        digest: None,
7074    }
7075}
7076
7077/// Create an AST node for coincident([expr1, expr2, ...])
7078pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7079    let elements = exprs.into_iter().collect::<Vec<_>>();
7080    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7081
7082    // Create array [expr1, expr2, ...]
7083    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7084        elements,
7085        digest: None,
7086        non_code_meta: Default::default(),
7087    })));
7088
7089    // Create coincident([...])
7090    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7092        unlabeled: Some(array_expr),
7093        arguments: Default::default(),
7094        digest: None,
7095        non_code_meta: Default::default(),
7096    })))
7097}
7098
7099/// Create an AST node for horizontal(line)
7100pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7101    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7102        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7103        unlabeled: Some(line_expr),
7104        arguments: Default::default(),
7105        digest: None,
7106        non_code_meta: Default::default(),
7107    })))
7108}
7109
7110/// Create an AST node for vertical(line)
7111pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7112    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7113        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7114        unlabeled: Some(line_expr),
7115        arguments: Default::default(),
7116        digest: None,
7117        non_code_meta: Default::default(),
7118    })))
7119}
7120
7121/// Create a member expression like object.property (e.g., line1.end)
7122pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7123    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7124        object: object_expr,
7125        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7126            name: ast::Node::no_src(ast::Identifier {
7127                name: property.to_string(),
7128                digest: None,
7129            }),
7130            path: Vec::new(),
7131            abs_path: false,
7132            digest: None,
7133        }))),
7134        computed: false,
7135        digest: None,
7136    })))
7137}
7138
7139pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7140    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7141        object: object_expr,
7142        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7143            ast::NumericLiteral {
7144                value: index as f64,
7145                suffix: NumericSuffix::None,
7146                raw: index.to_string(),
7147                digest: None,
7148            },
7149        )))),
7150        computed: true,
7151        digest: None,
7152    })))
7153}
7154
7155/// Create an AST node for `fixed([point, [x, y]])`.
7156fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7157    // Create [x, y] array literal.
7158    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7159        position.x,
7160    )?))));
7161    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7162        position.y,
7163    )?))));
7164    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7165        elements: vec![x_literal, y_literal],
7166        digest: None,
7167        non_code_meta: Default::default(),
7168    })));
7169
7170    // Create [point, [x, y]] outer array.
7171    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7172        elements: vec![point_expr, point_array],
7173        digest: None,
7174        non_code_meta: Default::default(),
7175    })));
7176
7177    // Create fixed([...])
7178    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7179        ast::CallExpressionKw {
7180            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7181            unlabeled: Some(array_expr),
7182            arguments: Default::default(),
7183            digest: None,
7184            non_code_meta: Default::default(),
7185        },
7186    ))))
7187}
7188
7189/// Create an AST node for equalLength([line1, line2, ...])
7190pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7191    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7192        elements: line_exprs,
7193        digest: None,
7194        non_code_meta: Default::default(),
7195    })));
7196
7197    // Create equalLength([...])
7198    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7199        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7200        unlabeled: Some(array_expr),
7201        arguments: Default::default(),
7202        digest: None,
7203        non_code_meta: Default::default(),
7204    })))
7205}
7206
7207/// Create an AST node for equalRadius([seg1, seg2, ...])
7208pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7209    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7210        elements: segment_exprs,
7211        digest: None,
7212        non_code_meta: Default::default(),
7213    })));
7214
7215    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7216        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7217        unlabeled: Some(array_expr),
7218        arguments: Default::default(),
7219        digest: None,
7220        non_code_meta: Default::default(),
7221    })))
7222}
7223
7224/// Create an AST node for tangent([seg1, seg2])
7225pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7226    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7227        elements: vec![seg1_expr, seg2_expr],
7228        digest: None,
7229        non_code_meta: Default::default(),
7230    })));
7231
7232    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7233        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7234        unlabeled: Some(array_expr),
7235        arguments: Default::default(),
7236        digest: None,
7237        non_code_meta: Default::default(),
7238    })))
7239}
7240
7241/// Create an AST node for symmetric([input1, input2], axis = line)
7242pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7243    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7244        elements: input_exprs,
7245        digest: None,
7246        non_code_meta: Default::default(),
7247    })));
7248    let arguments = vec![ast::LabeledArg {
7249        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7250        arg: axis_expr,
7251    }];
7252
7253    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7254        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7255        unlabeled: Some(array_expr),
7256        arguments,
7257        digest: None,
7258        non_code_meta: Default::default(),
7259    })))
7260}
7261
7262/// Create an AST node for midpoint(segment, point = point)
7263pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7264    let arguments = vec![ast::LabeledArg {
7265        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7266        arg: point_expr,
7267    }];
7268
7269    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7270        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7271        unlabeled: Some(segment_expr),
7272        arguments,
7273        digest: None,
7274        non_code_meta: Default::default(),
7275    })))
7276}
7277
7278/// The source range to attach to a diagnostic for `error` in the top-level
7279/// module. Source ranges are ordered innermost first and may point into
7280/// imported modules, so prefer the innermost range that is in the top-level
7281/// module; otherwise fall back to the innermost range.
7282fn issue_source_range(error: &KclError) -> SourceRange {
7283    let source_ranges = error.source_ranges();
7284    source_ranges
7285        .iter()
7286        .find(|range| range.is_top_level_module())
7287        .or_else(|| source_ranges.first())
7288        .copied()
7289        .unwrap_or_else(SourceRange::synthetic)
7290}
7291
7292#[cfg(test)]
7293mod tests {
7294    use std::sync;
7295
7296    use kcl_api::Distance;
7297    use kcl_api::Fixed;
7298    use kcl_api::FixedPoint;
7299    use kcl_api::Midpoint;
7300    use kcl_api::NumericSuffix;
7301    use kcl_api::Object;
7302    use kcl_api::Plane;
7303    use kcl_api::Sketch;
7304    use kcl_api::Tangent;
7305    use kcl_api::Vertical;
7306
7307    use super::*;
7308    use crate::engine::PlaneName;
7309    use crate::engine::engine_manager::EngineManager;
7310    use crate::execution::cache::SketchModeState;
7311
7312    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7313        for object in &scene_graph.objects {
7314            if let ObjectKind::Sketch(_) = &object.kind {
7315                return Some(object);
7316            }
7317        }
7318        None
7319    }
7320
7321    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7322        for object in &scene_graph.objects {
7323            if let ObjectKind::Face(_) = &object.kind {
7324                return Some(object);
7325            }
7326        }
7327        None
7328    }
7329
7330    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7331        for object in &scene_graph.objects {
7332            if matches!(&object.kind, ObjectKind::Wall(_)) {
7333                return Some(object.id);
7334            }
7335        }
7336        None
7337    }
7338
7339    fn find_cap_object_id_with_solid_output_index(
7340        scene_graph: &SceneGraph,
7341        cap_kind: kcl_api::CapKind,
7342        solid_output_index: usize,
7343    ) -> Option<ObjectId> {
7344        for object in &scene_graph.objects {
7345            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7346            {
7347                return Some(object.id);
7348            }
7349        }
7350        None
7351    }
7352
7353    #[test]
7354    fn issue_source_range_prefers_top_level_module() {
7355        use kcl_error::ModuleId;
7356
7357        let top = SourceRange::new(10, 20, ModuleId::default());
7358        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7359
7360        // Innermost frame is in an imported module; the diagnostic should
7361        // land on the innermost top-level range instead.
7362        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7363            "boom".to_owned(),
7364            vec![imported, top],
7365        ));
7366        assert_eq!(super::issue_source_range(&error), top);
7367
7368        // No top-level range at all: fall back to the innermost one.
7369        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7370        assert_eq!(super::issue_source_range(&error), imported);
7371
7372        // No ranges: synthetic.
7373        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7374        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7375    }
7376
7377    #[test]
7378    fn composite_constituent_sweeps_are_not_solid_outputs() {
7379        use kcl_api::artifact::ArtifactSweepMethod;
7380        use kcl_api::artifact::CompositeSolid;
7381        use kcl_api::artifact::CompositeSolidSubType;
7382        use kcl_api::artifact::Sweep;
7383        use kcl_api::artifact::SweepSubType;
7384
7385        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7386        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7387        let composite_id = ArtifactId::new(Uuid::new_v4());
7388        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7389        let sweep = |id| {
7390            Artifact::Sweep(Sweep {
7391                id,
7392                sub_type: SweepSubType::Extrusion,
7393                path_id: Some(ArtifactId::new(Uuid::new_v4())),
7394                surface_ids: Vec::new(),
7395                edge_ids: Vec::new(),
7396                code_ref: code_ref.clone(),
7397                source_sweep_id: None,
7398                trajectory_id: None,
7399                method: ArtifactSweepMethod::New,
7400                consumed: false,
7401                pattern_ids: Vec::new(),
7402            })
7403        };
7404        let mut artifacts = IndexMap::from([
7405            (first_sweep_id, sweep(first_sweep_id)),
7406            (second_sweep_id, sweep(second_sweep_id)),
7407        ]);
7408
7409        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7410        assert_eq!(
7411            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7412            Some(0)
7413        );
7414        assert_eq!(
7415            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7416            Some(1)
7417        );
7418
7419        artifacts.insert(
7420            composite_id,
7421            Artifact::CompositeSolid(CompositeSolid {
7422                id: composite_id,
7423                consumed: false,
7424                sub_type: CompositeSolidSubType::Union,
7425                output_index: None,
7426                solid_ids: vec![first_sweep_id, second_sweep_id],
7427                tool_ids: Vec::new(),
7428                code_ref,
7429                composite_solid_id: None,
7430                pattern_ids: Vec::new(),
7431            }),
7432        );
7433        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7434        assert_eq!(
7435            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7436            None
7437        );
7438        assert_eq!(
7439            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7440            None
7441        );
7442    }
7443
7444    #[test]
7445    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7446        let source = "\
7447region001 = region(point = [0.1, 0.1], sketch = s)
7448extrude001 = extrude(region001, length = 5)
7449revolve001 = revolve(region001, axis = Y)
7450sweep001 = sweep(region001, path = path001)
7451loft001 = loft(region001)
7452not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7453";
7454
7455        let program = Program::parse(source).unwrap().0.unwrap();
7456
7457        assert_eq!(
7458            region_name_from_sweep_variable(&program.ast, "extrude001"),
7459            Some("region001".to_owned())
7460        );
7461        assert_eq!(
7462            region_name_from_sweep_variable(&program.ast, "revolve001"),
7463            Some("region001".to_owned())
7464        );
7465        assert_eq!(
7466            region_name_from_sweep_variable(&program.ast, "sweep001"),
7467            Some("region001".to_owned())
7468        );
7469        assert_eq!(
7470            region_name_from_sweep_variable(&program.ast, "loft001"),
7471            Some("region001".to_owned())
7472        );
7473        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7474    }
7475
7476    #[track_caller]
7477    fn expect_sketch(object: &Object) -> &Sketch {
7478        if let ObjectKind::Sketch(sketch) = &object.kind {
7479            sketch
7480        } else {
7481            panic!("Object is not a sketch: {:?}", object);
7482        }
7483    }
7484
7485    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7486        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7487        let ObjectKind::Segment {
7488            segment: Segment::Point(point),
7489        } = &point_object.kind
7490        else {
7491            panic!("Object is not a point segment: {point_object:?}");
7492        };
7493        point.position.clone()
7494    }
7495
7496    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7497        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7498        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7499    }
7500
7501    /// Build a millimeter-valued point expression for concise sketch edit test
7502    /// setup.
7503    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7504        Point2d {
7505            x: Expr::Var(Number {
7506                value: x,
7507                units: NumericSuffix::Mm,
7508            }),
7509            y: Expr::Var(Number {
7510                value: y,
7511                units: NumericSuffix::Mm,
7512            }),
7513        }
7514    }
7515
7516    /// Build a millimeter-valued numeric point for comparing solved scene graph
7517    /// positions.
7518    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7519        Point2d {
7520            x: Number {
7521                value: x,
7522                units: NumericSuffix::Mm,
7523            },
7524            y: Number {
7525                value: y,
7526                units: NumericSuffix::Mm,
7527            },
7528        }
7529    }
7530
7531    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7532        LineCtor {
7533            start: Point2d {
7534                x: Expr::Number(Number { value: start_x, units }),
7535                y: Expr::Number(Number { value: start_y, units }),
7536            },
7537            end: Point2d {
7538                x: Expr::Number(Number { value: end_x, units }),
7539                y: Expr::Number(Number { value: end_y, units }),
7540            },
7541            construction: None,
7542        }
7543    }
7544
7545    async fn create_sketch_with_single_line(
7546        frontend: &mut FrontendState,
7547        ctx: &ExecutorContext,
7548        mock_ctx: &ExecutorContext,
7549        version: Version,
7550    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7551        frontend.program = Program::empty();
7552
7553        let sketch_args = SketchCtor {
7554            on: Plane::Default(PlaneName::Xy),
7555        };
7556        let (_src_delta, _scene_delta, sketch_id) = frontend
7557            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7558            .await
7559            .unwrap();
7560
7561        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7562        let (source_delta, scene_graph_delta) = frontend
7563            .add_segment(mock_ctx, version, sketch_id, segment, None)
7564            .await
7565            .unwrap();
7566        let line_id = *scene_graph_delta
7567            .new_objects
7568            .last()
7569            .expect("Expected line object id to be created");
7570
7571        (sketch_id, line_id, source_delta, scene_graph_delta)
7572    }
7573
7574    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7575        frontend.program = program.clone();
7576        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7577        frontend.update_state_after_exec(outcome, true);
7578    }
7579
7580    #[test]
7581    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7582        for (source, expected_message) in [
7583            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7584            (
7585                "3'",
7586                "Error parsing KCL source after editing: unterminated string literal",
7587            ),
7588        ] {
7589            let err = parse_frontend_mutation_source(
7590                source,
7591                "Error parsing KCL source after editing",
7592                "No AST produced after editing",
7593            )
7594            .expect_err("expected invalid KCL source to fail");
7595            let message = err.error.message();
7596
7597            assert_eq!(message, expected_message);
7598            assert!(!message.contains("CompilationIssue"));
7599            assert!(!message.contains("KclErrorDetails"));
7600            assert!(!message.contains("source_range"));
7601        }
7602    }
7603
7604    #[tokio::test(flavor = "multi_thread")]
7605    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7606        let initial_source = "\
7607sketch(on = XY) {
7608  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7609  distance([line1.start, line1.end]) == 10
7610}
7611";
7612        let program = Program::parse(initial_source).unwrap().0.unwrap();
7613
7614        let mut frontend = FrontendState::new();
7615        let mock_ctx = ExecutorContext::new_mock(None).await;
7616        let version = Version(0);
7617
7618        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7619        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7620        let sketch_id = sketch_object.id;
7621        let sketch = expect_sketch(sketch_object);
7622        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7623
7624        for (value, expected_message) in [
7625            ("**", "Invalid constraint value: Unexpected token: *"),
7626            ("3'", "Invalid constraint value: unterminated string literal"),
7627        ] {
7628            let err = frontend
7629                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7630                .await
7631                .expect_err("expected invalid constraint expression to fail");
7632            let message = err.error.message();
7633
7634            assert_eq!(message, expected_message);
7635            assert!(!message.contains("CompilationIssue"));
7636            assert!(!message.contains("KclErrorDetails"));
7637            assert!(!message.contains("source_range"));
7638        }
7639
7640        mock_ctx.close().await;
7641    }
7642
7643    #[tokio::test(flavor = "multi_thread")]
7644    async fn test_failed_edit_constraint_value_does_not_update_program() {
7645        let initial_source = "\
7646sketch(on = XY) {
7647  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7648  distance([line1.start, line1.end]) == 10
7649}
7650";
7651        let program = Program::parse(initial_source).unwrap().0.unwrap();
7652        let original_source = program.original_file_contents.clone();
7653
7654        let mut frontend = FrontendState::new();
7655        let mock_ctx = ExecutorContext::new_mock(None).await;
7656        let version = Version(0);
7657
7658        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7659        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7660        let sketch_id = sketch_object.id;
7661        let sketch = expect_sketch(sketch_object);
7662        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7663
7664        frontend
7665            .edit_constraint_value(
7666                &mock_ctx,
7667                version,
7668                sketch_id,
7669                constraint_id,
7670                "unknownDistance".to_owned(),
7671            )
7672            .await
7673            .expect_err("expected invalid constraint value to fail execution");
7674
7675        assert_eq!(frontend.program.original_file_contents, original_source);
7676        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7677
7678        mock_ctx.close().await;
7679    }
7680
7681    #[tokio::test(flavor = "multi_thread")]
7682    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7683        let initial_source = "\
7684arr = [0]
7685sketch(on = XY) {
7686  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7687  distance([line1.start, line1.end]) == 10
7688}
7689";
7690        let program = Program::parse(initial_source).unwrap().0.unwrap();
7691
7692        let mut frontend = FrontendState::new();
7693        let mock_ctx = ExecutorContext::new_mock(None).await;
7694        let version = Version(0);
7695
7696        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7697        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7698        let sketch_id = sketch_object.id;
7699        let sketch = expect_sketch(sketch_object);
7700        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7701
7702        // In sketch mode execution, an out-of-bounds array index is an error.
7703        // It should not fall back to the first element of the array the way
7704        // plain mock execution does.
7705        let err = frontend
7706            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7707            .await
7708            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7709        let message = err.error.message();
7710        assert!(
7711            message.contains("The array doesn't have any item at index 5"),
7712            "unexpected error message: {message}"
7713        );
7714
7715        mock_ctx.close().await;
7716    }
7717
7718    #[tokio::test(flavor = "multi_thread")]
7719    async fn test_sketch_checkpoint_round_trip_restores_state() {
7720        let mut frontend = FrontendState::new();
7721        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7722        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7723        let version = Version(0);
7724
7725        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7726            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7727
7728        let expected_source = source_delta.text.clone();
7729        let expected_scene_graph = frontend.scene_graph.clone();
7730        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7731        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7732
7733        let checkpoint_id = frontend
7734            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7735            .await
7736            .unwrap();
7737
7738        let edited_segments = vec![ExistingSegmentCtor {
7739            id: line_id,
7740            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7741        }];
7742        let (edited_source, _edited_scene) = frontend
7743            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7744            .await
7745            .unwrap();
7746        assert_ne!(edited_source.text, expected_source);
7747
7748        let restored = frontend
7749            .restore_sketch_checkpoint(&ctx.cache, checkpoint_id)
7750            .await
7751            .unwrap();
7752
7753        assert_eq!(restored.source_delta.text, expected_source);
7754        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7755        assert!(restored.scene_graph_delta.invalidates_ids);
7756        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7757        assert_eq!(frontend.scene_graph, expected_scene_graph);
7758        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7759
7760        ctx.close().await;
7761    }
7762
7763    #[tokio::test(flavor = "multi_thread")]
7764    async fn test_sketch_checkpoints_prune_oldest_entries() {
7765        let mut frontend = FrontendState::new();
7766        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7767        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7768        let version = Version(0);
7769
7770        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7771            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7772
7773        let mut checkpoint_ids = Vec::new();
7774        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7775            checkpoint_ids.push(
7776                frontend
7777                    .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7778                    .await
7779                    .unwrap(),
7780            );
7781        }
7782
7783        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7784        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7785
7786        let oldest_retained = checkpoint_ids[3];
7787        assert_eq!(
7788            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7789            Some(oldest_retained)
7790        );
7791
7792        let evicted_restore = frontend.restore_sketch_checkpoint(&ctx.cache, checkpoint_ids[0]).await;
7793        assert!(evicted_restore.is_err());
7794        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7795
7796        frontend
7797            .restore_sketch_checkpoint(&ctx.cache, *checkpoint_ids.last().unwrap())
7798            .await
7799            .unwrap();
7800
7801        ctx.close().await;
7802    }
7803
7804    #[tokio::test(flavor = "multi_thread")]
7805    async fn test_sketch_checkpoint_uses_explicit_session_cache() {
7806        let mut frontend = FrontendState::new();
7807        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
7808        let other = ExecutorContext::new_mock(None).await;
7809        let program = Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
7810        let outcome = ctx.run_with_caching(program).await.unwrap();
7811        let checkpoint = frontend.create_sketch_checkpoint(&ctx.cache, outcome).await.unwrap();
7812        ctx.cache.clear_mem_cache().await;
7813        assert!(ctx.cache.read_old_memory_var("x").await.is_none());
7814        frontend
7815            .restore_sketch_checkpoint(&ctx.cache, checkpoint)
7816            .await
7817            .unwrap();
7818        assert!(ctx.cache.read_old_memory_var("x").await.is_some());
7819        assert!(other.cache.read_old_memory_var("x").await.is_none());
7820        ctx.close().await;
7821        other.close().await;
7822    }
7823
7824    #[tokio::test(flavor = "multi_thread")]
7825    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7826        let mut frontend = FrontendState::new();
7827        let ctx = ExecutorContext::new_mock(None).await;
7828        let missing_checkpoint = SketchCheckpointId::new(999);
7829
7830        let err = frontend
7831            .restore_sketch_checkpoint(&ctx.cache, missing_checkpoint)
7832            .await
7833            .expect_err("Expected restore to fail for missing checkpoint");
7834
7835        assert!(err.msg.contains("Sketch checkpoint not found"));
7836    }
7837
7838    #[tokio::test(flavor = "multi_thread")]
7839    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7840        let mut frontend = FrontendState::new();
7841        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7842        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7843        let version = Version(0);
7844
7845        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7846            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7847
7848        let checkpoint_a = frontend
7849            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7850            .await
7851            .unwrap();
7852        let checkpoint_b = frontend
7853            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7854            .await
7855            .unwrap();
7856        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7857
7858        frontend.clear_sketch_checkpoints();
7859        assert!(frontend.sketch_checkpoints.is_empty());
7860        frontend
7861            .restore_sketch_checkpoint(&ctx.cache, checkpoint_a)
7862            .await
7863            .unwrap_err();
7864        frontend
7865            .restore_sketch_checkpoint(&ctx.cache, checkpoint_b)
7866            .await
7867            .unwrap_err();
7868
7869        ctx.close().await;
7870    }
7871
7872    #[tokio::test(flavor = "multi_thread")]
7873    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7874        let mut frontend = FrontendState::new();
7875        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7876        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7877        let version = Version(0);
7878
7879        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7880            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7881        let old_source = source_delta.text.clone();
7882        let old_checkpoint = frontend
7883            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7884            .await
7885            .unwrap();
7886        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7887
7888        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7889            .unwrap()
7890            .0
7891            .unwrap();
7892
7893        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7894        let SetProgramOutcome::Success {
7895            checkpoint_id: Some(new_checkpoint),
7896            ..
7897        } = result
7898        else {
7899            panic!("Expected Success with a fresh checkpoint baseline");
7900        };
7901
7902        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7903
7904        let old_restore = frontend
7905            .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7906            .await
7907            .unwrap();
7908        assert_eq!(old_restore.source_delta.text, old_source);
7909
7910        let new_restore = frontend
7911            .restore_sketch_checkpoint(&ctx.cache, new_checkpoint)
7912            .await
7913            .unwrap();
7914        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7915
7916        ctx.close().await;
7917    }
7918
7919    #[tokio::test(flavor = "multi_thread")]
7920    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7921        let mut frontend = FrontendState::new();
7922        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7923        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
7924        let version = Version(0);
7925
7926        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7927            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7928        let old_checkpoint = frontend
7929            .create_sketch_checkpoint(&ctx.cache, scene_graph_delta.exec_outcome.clone())
7930            .await
7931            .unwrap();
7932        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7933
7934        let failing_program = Program::parse(
7935            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7936        )
7937        .unwrap()
7938        .0
7939        .unwrap();
7940
7941        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7942        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7943        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7944        frontend
7945            .restore_sketch_checkpoint(&ctx.cache, old_checkpoint)
7946            .await
7947            .unwrap();
7948
7949        ctx.close().await;
7950    }
7951
7952    #[tokio::test(flavor = "multi_thread")]
7953    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7954        let mut frontend = FrontendState::new();
7955        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7956
7957        let program = Program::parse(
7958            "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",
7959        )
7960        .unwrap()
7961        .0
7962        .unwrap();
7963        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7964        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7965            panic!("Expected successful baseline program execution");
7966        };
7967
7968        ctx.cache.clear_mem_cache().await;
7969        assert!(ctx.cache.read_old_memory().await.is_none());
7970
7971        let checkpoint_without_mock_memory = frontend
7972            .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7973            .await
7974            .unwrap();
7975
7976        ctx.cache.write_old_memory(SketchModeState::new_for_tests()).await;
7977        assert!(ctx.cache.read_old_memory().await.is_some());
7978
7979        let checkpoint_with_mock_memory = frontend
7980            .create_sketch_checkpoint(&ctx.cache, (*exec_outcome).clone())
7981            .await
7982            .unwrap();
7983
7984        ctx.cache.clear_mem_cache().await;
7985        assert!(ctx.cache.read_old_memory().await.is_none());
7986
7987        frontend
7988            .restore_sketch_checkpoint(&ctx.cache, checkpoint_with_mock_memory)
7989            .await
7990            .unwrap();
7991        assert!(ctx.cache.read_old_memory().await.is_some());
7992
7993        frontend
7994            .restore_sketch_checkpoint(&ctx.cache, checkpoint_without_mock_memory)
7995            .await
7996            .unwrap();
7997        assert!(ctx.cache.read_old_memory().await.is_none());
7998
7999        ctx.close().await;
8000    }
8001
8002    #[tokio::test(flavor = "multi_thread")]
8003    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8004        let source = "\
8005sketch(on = XY) {
8006  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8007}
8008
8009bad = missing_name
8010";
8011        let program = Program::parse(source).unwrap().0.unwrap();
8012
8013        let mut frontend = FrontendState::new();
8014
8015        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8016        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8017        let version = Version(0);
8018        let project_id = ProjectId(0);
8019        let file_id = FileId(0);
8020
8021        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8022            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8023        };
8024
8025        let sketch_id = frontend
8026            .scene_graph
8027            .objects
8028            .iter()
8029            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8030            .expect("Expected sketch object from errored hack_set_program");
8031
8032        frontend
8033            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8034            .await
8035            .unwrap();
8036
8037        ctx.close().await;
8038        mock_ctx.close().await;
8039    }
8040
8041    #[tokio::test(flavor = "multi_thread")]
8042    async fn test_new_sketch_add_point_edit_point() {
8043        let program = Program::empty();
8044
8045        let mut frontend = FrontendState::new();
8046        frontend.program = program;
8047
8048        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8049        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8050        let version = Version(0);
8051
8052        let sketch_args = SketchCtor {
8053            on: Plane::Default(PlaneName::Xy),
8054        };
8055        let (_src_delta, scene_delta, sketch_id) = frontend
8056            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8057            .await
8058            .unwrap();
8059        assert_eq!(sketch_id, ObjectId(1));
8060        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8061        let sketch_object = &scene_delta.new_graph.objects[1];
8062        assert_eq!(sketch_object.id, ObjectId(1));
8063        assert_eq!(
8064            sketch_object.kind,
8065            ObjectKind::Sketch(Sketch {
8066                args: SketchCtor {
8067                    on: Plane::Default(PlaneName::Xy)
8068                },
8069                plane: ObjectId(0),
8070                segments: vec![],
8071                constraints: vec![],
8072            })
8073        );
8074        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8075
8076        let point_ctor = PointCtor {
8077            position: Point2d {
8078                x: Expr::Number(Number {
8079                    value: 1.0,
8080                    units: NumericSuffix::Inch,
8081                }),
8082                y: Expr::Number(Number {
8083                    value: 2.0,
8084                    units: NumericSuffix::Inch,
8085                }),
8086            },
8087        };
8088        let segment = SegmentCtor::Point(point_ctor);
8089        let (src_delta, scene_delta) = frontend
8090            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091            .await
8092            .unwrap();
8093        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8094        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8095        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8096        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8097            assert_eq!(scene_object.id.0, i);
8098        }
8099
8100        let point_id = *scene_delta.new_objects.last().unwrap();
8101
8102        let point_ctor = PointCtor {
8103            position: Point2d {
8104                x: Expr::Number(Number {
8105                    value: 3.0,
8106                    units: NumericSuffix::Inch,
8107                }),
8108                y: Expr::Number(Number {
8109                    value: 4.0,
8110                    units: NumericSuffix::Inch,
8111                }),
8112            },
8113        };
8114        let segments = vec![ExistingSegmentCtor {
8115            id: point_id,
8116            ctor: SegmentCtor::Point(point_ctor),
8117        }];
8118        let (src_delta, scene_delta) = frontend
8119            .edit_segments(&mock_ctx, version, sketch_id, segments)
8120            .await
8121            .unwrap();
8122        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8123        assert_eq!(scene_delta.new_objects, vec![]);
8124        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8125
8126        ctx.close().await;
8127        mock_ctx.close().await;
8128    }
8129
8130    #[tokio::test(flavor = "multi_thread")]
8131    async fn test_new_sketch_add_line_edit_line() {
8132        let program = Program::empty();
8133
8134        let mut frontend = FrontendState::new();
8135        frontend.program = program;
8136
8137        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8138        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8139        let version = Version(0);
8140
8141        let sketch_args = SketchCtor {
8142            on: Plane::Default(PlaneName::Xy),
8143        };
8144        let (_src_delta, scene_delta, sketch_id) = frontend
8145            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8146            .await
8147            .unwrap();
8148        assert_eq!(sketch_id, ObjectId(1));
8149        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8150        let sketch_object = &scene_delta.new_graph.objects[1];
8151        assert_eq!(sketch_object.id, ObjectId(1));
8152        assert_eq!(
8153            sketch_object.kind,
8154            ObjectKind::Sketch(Sketch {
8155                args: SketchCtor {
8156                    on: Plane::Default(PlaneName::Xy)
8157                },
8158                plane: ObjectId(0),
8159                segments: vec![],
8160                constraints: vec![],
8161            })
8162        );
8163        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8164
8165        let line_ctor = LineCtor {
8166            start: Point2d {
8167                x: Expr::Number(Number {
8168                    value: 0.0,
8169                    units: NumericSuffix::Mm,
8170                }),
8171                y: Expr::Number(Number {
8172                    value: 0.0,
8173                    units: NumericSuffix::Mm,
8174                }),
8175            },
8176            end: Point2d {
8177                x: Expr::Number(Number {
8178                    value: 10.0,
8179                    units: NumericSuffix::Mm,
8180                }),
8181                y: Expr::Number(Number {
8182                    value: 10.0,
8183                    units: NumericSuffix::Mm,
8184                }),
8185            },
8186            construction: None,
8187        };
8188        let segment = SegmentCtor::Line(line_ctor);
8189        let (src_delta, scene_delta) = frontend
8190            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8191            .await
8192            .unwrap();
8193        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8194        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8195        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8196        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8197            assert_eq!(scene_object.id.0, i);
8198        }
8199
8200        // The new objects are the end points and then the line.
8201        let line = *scene_delta.new_objects.last().unwrap();
8202
8203        let line_ctor = LineCtor {
8204            start: Point2d {
8205                x: Expr::Number(Number {
8206                    value: 1.0,
8207                    units: NumericSuffix::Mm,
8208                }),
8209                y: Expr::Number(Number {
8210                    value: 2.0,
8211                    units: NumericSuffix::Mm,
8212                }),
8213            },
8214            end: Point2d {
8215                x: Expr::Number(Number {
8216                    value: 13.0,
8217                    units: NumericSuffix::Mm,
8218                }),
8219                y: Expr::Number(Number {
8220                    value: 14.0,
8221                    units: NumericSuffix::Mm,
8222                }),
8223            },
8224            construction: None,
8225        };
8226        let segments = vec![ExistingSegmentCtor {
8227            id: line,
8228            ctor: SegmentCtor::Line(line_ctor),
8229        }];
8230        let (src_delta, scene_delta) = frontend
8231            .edit_segments(&mock_ctx, version, sketch_id, segments)
8232            .await
8233            .unwrap();
8234        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8235        assert_eq!(scene_delta.new_objects, vec![]);
8236        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8237
8238        ctx.close().await;
8239        mock_ctx.close().await;
8240    }
8241
8242    #[tokio::test(flavor = "multi_thread")]
8243    async fn test_new_sketch_add_arc_edit_arc() {
8244        let program = Program::empty();
8245
8246        let mut frontend = FrontendState::new();
8247        frontend.program = program;
8248
8249        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8250        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8251        let version = Version(0);
8252
8253        let sketch_args = SketchCtor {
8254            on: Plane::Default(PlaneName::Xy),
8255        };
8256        let (_src_delta, scene_delta, sketch_id) = frontend
8257            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8258            .await
8259            .unwrap();
8260        assert_eq!(sketch_id, ObjectId(1));
8261        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8262        let sketch_object = &scene_delta.new_graph.objects[1];
8263        assert_eq!(sketch_object.id, ObjectId(1));
8264        assert_eq!(
8265            sketch_object.kind,
8266            ObjectKind::Sketch(Sketch {
8267                args: SketchCtor {
8268                    on: Plane::Default(PlaneName::Xy),
8269                },
8270                plane: ObjectId(0),
8271                segments: vec![],
8272                constraints: vec![],
8273            })
8274        );
8275        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8276
8277        let arc_ctor = ArcCtor {
8278            start: Point2d {
8279                x: Expr::Var(Number {
8280                    value: 0.0,
8281                    units: NumericSuffix::Mm,
8282                }),
8283                y: Expr::Var(Number {
8284                    value: 0.0,
8285                    units: NumericSuffix::Mm,
8286                }),
8287            },
8288            end: Point2d {
8289                x: Expr::Var(Number {
8290                    value: 10.0,
8291                    units: NumericSuffix::Mm,
8292                }),
8293                y: Expr::Var(Number {
8294                    value: 10.0,
8295                    units: NumericSuffix::Mm,
8296                }),
8297            },
8298            center: Point2d {
8299                x: Expr::Var(Number {
8300                    value: 10.0,
8301                    units: NumericSuffix::Mm,
8302                }),
8303                y: Expr::Var(Number {
8304                    value: 0.0,
8305                    units: NumericSuffix::Mm,
8306                }),
8307            },
8308            direction: None,
8309            construction: None,
8310        };
8311        let segment = SegmentCtor::Arc(arc_ctor);
8312        let (src_delta, scene_delta) = frontend
8313            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8314            .await
8315            .unwrap();
8316        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8317        assert_eq!(
8318            scene_delta.new_objects,
8319            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8320        );
8321        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8322            assert_eq!(scene_object.id.0, i);
8323        }
8324        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8325
8326        // The new objects are the end points, the center, and then the arc.
8327        let arc = *scene_delta.new_objects.last().unwrap();
8328
8329        let arc_ctor = ArcCtor {
8330            start: Point2d {
8331                x: Expr::Var(Number {
8332                    value: 1.0,
8333                    units: NumericSuffix::Mm,
8334                }),
8335                y: Expr::Var(Number {
8336                    value: 2.0,
8337                    units: NumericSuffix::Mm,
8338                }),
8339            },
8340            end: Point2d {
8341                x: Expr::Var(Number {
8342                    value: 13.0,
8343                    units: NumericSuffix::Mm,
8344                }),
8345                y: Expr::Var(Number {
8346                    value: 14.0,
8347                    units: NumericSuffix::Mm,
8348                }),
8349            },
8350            center: Point2d {
8351                x: Expr::Var(Number {
8352                    value: 13.0,
8353                    units: NumericSuffix::Mm,
8354                }),
8355                y: Expr::Var(Number {
8356                    value: 2.0,
8357                    units: NumericSuffix::Mm,
8358                }),
8359            },
8360            direction: None,
8361            construction: None,
8362        };
8363        let segments = vec![ExistingSegmentCtor {
8364            id: arc,
8365            ctor: SegmentCtor::Arc(arc_ctor),
8366        }];
8367        let (src_delta, scene_delta) = frontend
8368            .edit_segments(&mock_ctx, version, sketch_id, segments)
8369            .await
8370            .unwrap();
8371        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8372        assert_eq!(scene_delta.new_objects, vec![]);
8373        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8374
8375        ctx.close().await;
8376        mock_ctx.close().await;
8377    }
8378
8379    #[tokio::test(flavor = "multi_thread")]
8380    async fn test_new_sketch_add_circle_edit_circle() {
8381        let program = Program::empty();
8382
8383        let mut frontend = FrontendState::new();
8384        frontend.program = program;
8385
8386        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8387        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8388        let version = Version(0);
8389
8390        let sketch_args = SketchCtor {
8391            on: Plane::Default(PlaneName::Xy),
8392        };
8393        let (_src_delta, _scene_delta, sketch_id) = frontend
8394            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8395            .await
8396            .unwrap();
8397
8398        // Add a circle segment.
8399        let circle_ctor = CircleCtor {
8400            start: Point2d {
8401                x: Expr::Var(Number {
8402                    value: 5.0,
8403                    units: NumericSuffix::Mm,
8404                }),
8405                y: Expr::Var(Number {
8406                    value: 0.0,
8407                    units: NumericSuffix::Mm,
8408                }),
8409            },
8410            center: Point2d {
8411                x: Expr::Var(Number {
8412                    value: 0.0,
8413                    units: NumericSuffix::Mm,
8414                }),
8415                y: Expr::Var(Number {
8416                    value: 0.0,
8417                    units: NumericSuffix::Mm,
8418                }),
8419            },
8420            construction: None,
8421        };
8422        let segment = SegmentCtor::Circle(circle_ctor);
8423        let (src_delta, scene_delta) = frontend
8424            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8425            .await
8426            .unwrap();
8427        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8428        // The new objects are start, center, and then the circle segment.
8429        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8430        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8431
8432        let circle = *scene_delta.new_objects.last().unwrap();
8433
8434        // Edit the circle segment.
8435        let circle_ctor = CircleCtor {
8436            start: Point2d {
8437                x: Expr::Var(Number {
8438                    value: 10.0,
8439                    units: NumericSuffix::Mm,
8440                }),
8441                y: Expr::Var(Number {
8442                    value: 0.0,
8443                    units: NumericSuffix::Mm,
8444                }),
8445            },
8446            center: Point2d {
8447                x: Expr::Var(Number {
8448                    value: 3.0,
8449                    units: NumericSuffix::Mm,
8450                }),
8451                y: Expr::Var(Number {
8452                    value: 4.0,
8453                    units: NumericSuffix::Mm,
8454                }),
8455            },
8456            construction: None,
8457        };
8458        let segments = vec![ExistingSegmentCtor {
8459            id: circle,
8460            ctor: SegmentCtor::Circle(circle_ctor),
8461        }];
8462        let (src_delta, scene_delta) = frontend
8463            .edit_segments(&mock_ctx, version, sketch_id, segments)
8464            .await
8465            .unwrap();
8466        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8467        assert_eq!(scene_delta.new_objects, vec![]);
8468        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8469
8470        ctx.close().await;
8471        mock_ctx.close().await;
8472    }
8473
8474    #[tokio::test(flavor = "multi_thread")]
8475    async fn test_delete_circle() {
8476        let initial_source = "sketch001 = sketch(on = XY) {
8477  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8478}
8479";
8480
8481        let program = Program::parse(initial_source).unwrap().0.unwrap();
8482        let mut frontend = FrontendState::new();
8483
8484        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8485        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8486        let version = Version(0);
8487
8488        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490        let sketch_id = sketch_object.id;
8491        let sketch = expect_sketch(sketch_object);
8492
8493        // The sketch should have 3 segments: start point, center point, and the circle.
8494        assert_eq!(sketch.segments.len(), 3);
8495        let circle_id = sketch.segments[2];
8496
8497        // Delete the circle.
8498        let (src_delta, scene_delta) = frontend
8499            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8500            .await
8501            .unwrap();
8502        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8503        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8504        let new_sketch = expect_sketch(new_sketch_object);
8505        assert_eq!(new_sketch.segments.len(), 0);
8506
8507        ctx.close().await;
8508        mock_ctx.close().await;
8509    }
8510
8511    #[tokio::test(flavor = "multi_thread")]
8512    async fn test_edit_circle_via_point() {
8513        let initial_source = "sketch001 = sketch(on = XY) {
8514  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8515}
8516";
8517
8518        let program = Program::parse(initial_source).unwrap().0.unwrap();
8519        let mut frontend = FrontendState::new();
8520
8521        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8522        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8523        let version = Version(0);
8524
8525        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8526        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8527        let sketch_id = sketch_object.id;
8528        let sketch = expect_sketch(sketch_object);
8529
8530        // Find the circle segment and its start point.
8531        let circle_id = sketch
8532            .segments
8533            .iter()
8534            .copied()
8535            .find(|seg_id| {
8536                matches!(
8537                    &frontend.scene_graph.objects[seg_id.0].kind,
8538                    ObjectKind::Segment {
8539                        segment: Segment::Circle(_)
8540                    }
8541                )
8542            })
8543            .expect("Expected a circle segment in sketch");
8544        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8545        let ObjectKind::Segment {
8546            segment: Segment::Circle(circle),
8547        } = &circle_object.kind
8548        else {
8549            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8550        };
8551        let start_point_id = circle.start;
8552
8553        // Edit the start point via SegmentCtor::Point.
8554        let segments = vec![ExistingSegmentCtor {
8555            id: start_point_id,
8556            ctor: SegmentCtor::Point(PointCtor {
8557                position: Point2d {
8558                    x: Expr::Var(Number {
8559                        value: 7.0,
8560                        units: NumericSuffix::Mm,
8561                    }),
8562                    y: Expr::Var(Number {
8563                        value: 1.0,
8564                        units: NumericSuffix::Mm,
8565                    }),
8566                },
8567            }),
8568        }];
8569        let (src_delta, _scene_delta) = frontend
8570            .edit_segments(&mock_ctx, version, sketch_id, segments)
8571            .await
8572            .unwrap();
8573        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8574
8575        ctx.close().await;
8576        mock_ctx.close().await;
8577    }
8578
8579    #[tokio::test(flavor = "multi_thread")]
8580    async fn test_add_line_when_sketch_block_uses_variable() {
8581        let initial_source = "s = sketch(on = XY) {}
8582";
8583
8584        let program = Program::parse(initial_source).unwrap().0.unwrap();
8585
8586        let mut frontend = FrontendState::new();
8587
8588        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8589        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8590        let version = Version(0);
8591
8592        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594        let sketch_id = sketch_object.id;
8595
8596        let line_ctor = LineCtor {
8597            start: Point2d {
8598                x: Expr::Number(Number {
8599                    value: 0.0,
8600                    units: NumericSuffix::Mm,
8601                }),
8602                y: Expr::Number(Number {
8603                    value: 0.0,
8604                    units: NumericSuffix::Mm,
8605                }),
8606            },
8607            end: Point2d {
8608                x: Expr::Number(Number {
8609                    value: 10.0,
8610                    units: NumericSuffix::Mm,
8611                }),
8612                y: Expr::Number(Number {
8613                    value: 10.0,
8614                    units: NumericSuffix::Mm,
8615                }),
8616            },
8617            construction: None,
8618        };
8619        let segment = SegmentCtor::Line(line_ctor);
8620        let (src_delta, scene_delta) = frontend
8621            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8622            .await
8623            .unwrap();
8624        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8625        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8626        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628        ctx.close().await;
8629        mock_ctx.close().await;
8630    }
8631
8632    #[tokio::test(flavor = "multi_thread")]
8633    async fn test_new_sketch_add_line_delete_sketch() {
8634        let program = Program::empty();
8635
8636        let mut frontend = FrontendState::new();
8637        frontend.program = program;
8638
8639        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8640        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8641        let version = Version(0);
8642
8643        let sketch_args = SketchCtor {
8644            on: Plane::Default(PlaneName::Xy),
8645        };
8646        let (_src_delta, scene_delta, sketch_id) = frontend
8647            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8648            .await
8649            .unwrap();
8650        assert_eq!(sketch_id, ObjectId(1));
8651        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8652        let sketch_object = &scene_delta.new_graph.objects[1];
8653        assert_eq!(sketch_object.id, ObjectId(1));
8654        assert_eq!(
8655            sketch_object.kind,
8656            ObjectKind::Sketch(Sketch {
8657                args: SketchCtor {
8658                    on: Plane::Default(PlaneName::Xy)
8659                },
8660                plane: ObjectId(0),
8661                segments: vec![],
8662                constraints: vec![],
8663            })
8664        );
8665        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8666
8667        let line_ctor = LineCtor {
8668            start: Point2d {
8669                x: Expr::Number(Number {
8670                    value: 0.0,
8671                    units: NumericSuffix::Mm,
8672                }),
8673                y: Expr::Number(Number {
8674                    value: 0.0,
8675                    units: NumericSuffix::Mm,
8676                }),
8677            },
8678            end: Point2d {
8679                x: Expr::Number(Number {
8680                    value: 10.0,
8681                    units: NumericSuffix::Mm,
8682                }),
8683                y: Expr::Number(Number {
8684                    value: 10.0,
8685                    units: NumericSuffix::Mm,
8686                }),
8687            },
8688            construction: None,
8689        };
8690        let segment = SegmentCtor::Line(line_ctor);
8691        let (src_delta, scene_delta) = frontend
8692            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8693            .await
8694            .unwrap();
8695        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8696        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8697
8698        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8699        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8700        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8701
8702        ctx.close().await;
8703        mock_ctx.close().await;
8704    }
8705
8706    #[tokio::test(flavor = "multi_thread")]
8707    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8708        let initial_source = "s = sketch(on = XY) {}
8709";
8710
8711        let program = Program::parse(initial_source).unwrap().0.unwrap();
8712
8713        let mut frontend = FrontendState::new();
8714
8715        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8716        let version = Version(0);
8717
8718        frontend.hack_set_program(&ctx, program).await.unwrap();
8719        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8720        let sketch_id = sketch_object.id;
8721
8722        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8723        insta::assert_snapshot!(
8724            "test_delete_sketch_when_sketch_block_uses_variable",
8725            src_delta.text.as_str()
8726        );
8727        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8728
8729        ctx.close().await;
8730    }
8731
8732    #[tokio::test(flavor = "multi_thread")]
8733    async fn test_delete_sketch_after_comment() {
8734        let initial_source = "sketch001 = sketch(on = XZ) {
8735}
8736";
8737
8738        let program = Program::parse(initial_source).unwrap().0.unwrap();
8739        let mut frontend = FrontendState::new();
8740
8741        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8742        let version = Version(0);
8743
8744        frontend.hack_set_program(&ctx, program).await.unwrap();
8745        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8746        let sketch_id = sketch_object.id;
8747        let original_source = sketch_object.source.clone();
8748
8749        let commented_source = "// test 1
8750sketch001 = sketch(on = XZ) {
8751}
8752";
8753        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8754        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8755
8756        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8757        assert_eq!(cached_sketch_object.source, original_source);
8758
8759        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8760        assert!(
8761            !src_delta.text.contains("sketch001"),
8762            "sketch was not deleted: {}",
8763            src_delta.text
8764        );
8765        // The leading line comment must survive deletion.
8766        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8767        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8768
8769        ctx.close().await;
8770    }
8771
8772    #[tokio::test(flavor = "multi_thread")]
8773    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8774        let initial_source = "sketch001 = sketch(on = XZ) {
8775}
8776foo = 1
8777";
8778
8779        let program = Program::parse(initial_source).unwrap().0.unwrap();
8780        let mut frontend = FrontendState::new();
8781
8782        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8783        let version = Version(0);
8784
8785        frontend.hack_set_program(&ctx, program).await.unwrap();
8786        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8787        let sketch_id = sketch_object.id;
8788
8789        let commented_source = "// keep me
8790sketch001 = sketch(on = XZ) {
8791}
8792foo = 1
8793";
8794        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8795        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8796
8797        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8798        // The leading comment should remain, now attached to the following body item.
8799        insta::assert_snapshot!(
8800            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8801            src_delta.text.as_str()
8802        );
8803
8804        ctx.close().await;
8805    }
8806
8807    #[tokio::test(flavor = "multi_thread")]
8808    async fn test_delete_segment_preserves_pre_comment() {
8809        let initial_source = "\
8810sketch(on = XY) {
8811  point(at = [var 1, var 2])
8812  // describe the middle point
8813  point(at = [var 3, var 4])
8814  point(at = [var 5, var 6])
8815}
8816";
8817
8818        let program = Program::parse(initial_source).unwrap().0.unwrap();
8819        let mut frontend = FrontendState::new();
8820
8821        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8822        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8823        let version = Version(0);
8824
8825        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8826        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8827        let sketch_id = sketch_object.id;
8828        let sketch = expect_sketch(sketch_object);
8829
8830        let middle_point_id = *sketch.segments.get(1).unwrap();
8831
8832        let (src_delta, _scene_delta) = frontend
8833            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8834            .await
8835            .unwrap();
8836        // The line comment on the line above the deleted point must be preserved.
8837        // It is reattached to the next surviving body item.
8838        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8839
8840        ctx.close().await;
8841        mock_ctx.close().await;
8842    }
8843
8844    #[tokio::test(flavor = "multi_thread")]
8845    async fn test_delete_last_segment_preserves_pre_comment() {
8846        let initial_source = "\
8847sketch(on = XY) {
8848  point(at = [var 1, var 2])
8849  // describe the trailing point
8850  point(at = [var 3, var 4])
8851}
8852";
8853
8854        let program = Program::parse(initial_source).unwrap().0.unwrap();
8855        let mut frontend = FrontendState::new();
8856
8857        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8858        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8859        let version = Version(0);
8860
8861        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8862        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8863        let sketch_id = sketch_object.id;
8864        let sketch = expect_sketch(sketch_object);
8865
8866        let last_point_id = *sketch.segments.last().unwrap();
8867
8868        let (src_delta, _scene_delta) = frontend
8869            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8870            .await
8871            .unwrap();
8872        // No following item to attach to; the comment is kept inside the sketch
8873        // block as trailing non-code metadata so the user does not lose it.
8874        insta::assert_snapshot!(
8875            "test_delete_last_segment_preserves_pre_comment",
8876            src_delta.text.as_str()
8877        );
8878
8879        ctx.close().await;
8880        mock_ctx.close().await;
8881    }
8882
8883    #[tokio::test(flavor = "multi_thread")]
8884    async fn test_delete_segment_drops_inline_trailing_comment() {
8885        let initial_source = "\
8886sketch(on = XY) {
8887  point(at = [var 1, var 2])
8888  point(at = [var 3, var 4]) // same-line note that gets dropped
8889  point(at = [var 5, var 6])
8890}
8891";
8892
8893        let program = Program::parse(initial_source).unwrap().0.unwrap();
8894        let mut frontend = FrontendState::new();
8895
8896        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8897        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8898        let version = Version(0);
8899
8900        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8901        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8902        let sketch_id = sketch_object.id;
8903        let sketch = expect_sketch(sketch_object);
8904
8905        let middle_point_id = *sketch.segments.get(1).unwrap();
8906
8907        let (src_delta, _scene_delta) = frontend
8908            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8909            .await
8910            .unwrap();
8911        // The same-line trailing comment is removed along with the deleted code.
8912        assert!(
8913            !src_delta.text.contains("same-line note"),
8914            "inline comment should have been removed: {}",
8915            src_delta.text
8916        );
8917
8918        ctx.close().await;
8919        mock_ctx.close().await;
8920    }
8921
8922    #[tokio::test(flavor = "multi_thread")]
8923    async fn test_delete_segments_preserves_block_comments_across_positions() {
8924        // One test exercising several `delete_body_item_preserving_pre_comments`
8925        // branches at once with `/* ... */` block comments:
8926        //   - first point: leading block comment must migrate to the next item.
8927        //   - first point: same-line trailing block comment must be dropped.
8928        //   - middle point: leading block comment must stay attached after migration.
8929        //   - last point: leading block comment, with no surviving next item,
8930        //     must be converted into a trailing NonCodeNode.
8931        let initial_source = "\
8932sketch(on = XY) {
8933  /* above first - moves to middle */
8934  point(at = [var 1, var 2]) /* same-line on first - dropped */
8935  /* above middle - stays */
8936  point(at = [var 3, var 4])
8937  /* above last - moves to trailing meta */
8938  point(at = [var 5, var 6])
8939}
8940";
8941
8942        let program = Program::parse(initial_source).unwrap().0.unwrap();
8943        let mut frontend = FrontendState::new();
8944
8945        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8946        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8947        let version = Version(0);
8948
8949        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8950        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8951        let sketch_id = sketch_object.id;
8952        let sketch = expect_sketch(sketch_object);
8953
8954        let first_point_id = *sketch.segments.first().unwrap();
8955        let last_point_id = *sketch.segments.last().unwrap();
8956
8957        let (src_delta, _scene_delta) = frontend
8958            .delete_objects(
8959                &mock_ctx,
8960                version,
8961                sketch_id,
8962                Vec::new(),
8963                vec![first_point_id, last_point_id],
8964            )
8965            .await
8966            .unwrap();
8967        insta::assert_snapshot!(
8968            "test_delete_segments_preserves_block_comments_across_positions",
8969            src_delta.text.as_str()
8970        );
8971
8972        ctx.close().await;
8973        mock_ctx.close().await;
8974    }
8975
8976    #[tokio::test(flavor = "multi_thread")]
8977    async fn test_edit_line_when_editing_its_start_point() {
8978        let initial_source = "\
8979sketch(on = XY) {
8980  line(start = [var 1, var 2], end = [var 3, var 4])
8981}
8982";
8983
8984        let program = Program::parse(initial_source).unwrap().0.unwrap();
8985
8986        let mut frontend = FrontendState::new();
8987
8988        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8989        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
8990        let version = Version(0);
8991
8992        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8993        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8994        let sketch_id = sketch_object.id;
8995        let sketch = expect_sketch(sketch_object);
8996
8997        let point_id = *sketch.segments.first().unwrap();
8998
8999        let point_ctor = PointCtor {
9000            position: Point2d {
9001                x: Expr::Var(Number {
9002                    value: 5.0,
9003                    units: NumericSuffix::Inch,
9004                }),
9005                y: Expr::Var(Number {
9006                    value: 6.0,
9007                    units: NumericSuffix::Inch,
9008                }),
9009            },
9010        };
9011        let segments = vec![ExistingSegmentCtor {
9012            id: point_id,
9013            ctor: SegmentCtor::Point(point_ctor),
9014        }];
9015        let (src_delta, scene_delta) = frontend
9016            .edit_segments(&mock_ctx, version, sketch_id, segments)
9017            .await
9018            .unwrap();
9019        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9020        assert_eq!(scene_delta.new_objects, vec![]);
9021        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9022
9023        ctx.close().await;
9024        mock_ctx.close().await;
9025    }
9026
9027    #[tokio::test(flavor = "multi_thread")]
9028    async fn test_edit_line_when_editing_its_end_point() {
9029        let initial_source = "\
9030sketch(on = XY) {
9031  line(start = [var 1, var 2], end = [var 3, var 4])
9032}
9033";
9034
9035        let program = Program::parse(initial_source).unwrap().0.unwrap();
9036
9037        let mut frontend = FrontendState::new();
9038
9039        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9040        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9041        let version = Version(0);
9042
9043        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9044        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9045        let sketch_id = sketch_object.id;
9046        let sketch = expect_sketch(sketch_object);
9047        let point_id = *sketch.segments.get(1).unwrap();
9048
9049        let point_ctor = PointCtor {
9050            position: Point2d {
9051                x: Expr::Var(Number {
9052                    value: 5.0,
9053                    units: NumericSuffix::Inch,
9054                }),
9055                y: Expr::Var(Number {
9056                    value: 6.0,
9057                    units: NumericSuffix::Inch,
9058                }),
9059            },
9060        };
9061        let segments = vec![ExistingSegmentCtor {
9062            id: point_id,
9063            ctor: SegmentCtor::Point(point_ctor),
9064        }];
9065        let (src_delta, scene_delta) = frontend
9066            .edit_segments(&mock_ctx, version, sketch_id, segments)
9067            .await
9068            .unwrap();
9069        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9070        assert_eq!(scene_delta.new_objects, vec![]);
9071        assert_eq!(
9072            scene_delta.new_graph.objects.len(),
9073            5,
9074            "{:#?}",
9075            scene_delta.new_graph.objects
9076        );
9077
9078        ctx.close().await;
9079        mock_ctx.close().await;
9080    }
9081
9082    #[tokio::test(flavor = "multi_thread")]
9083    async fn test_edit_line_with_coincident_feedback() {
9084        let initial_source = "\
9085sketch(on = XY) {
9086  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9087  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9088  fixed([line1.start, [0, 0]])
9089  coincident([line1.end, line2.start])
9090  equalLength([line1, line2])
9091}
9092";
9093
9094        let program = Program::parse(initial_source).unwrap().0.unwrap();
9095
9096        let mut frontend = FrontendState::new();
9097
9098        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9099        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9100        let version = Version(0);
9101
9102        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9103        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9104        let sketch_id = sketch_object.id;
9105        let sketch = expect_sketch(sketch_object);
9106        let line2_end_id = *sketch.segments.get(4).unwrap();
9107
9108        let segments = vec![ExistingSegmentCtor {
9109            id: line2_end_id,
9110            ctor: SegmentCtor::Point(PointCtor {
9111                position: Point2d {
9112                    x: Expr::Var(Number {
9113                        value: 9.0,
9114                        units: NumericSuffix::None,
9115                    }),
9116                    y: Expr::Var(Number {
9117                        value: 10.0,
9118                        units: NumericSuffix::None,
9119                    }),
9120                },
9121            }),
9122        }];
9123        let (src_delta, scene_delta) = frontend
9124            .edit_segments(&mock_ctx, version, sketch_id, segments)
9125            .await
9126            .unwrap();
9127        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9128        assert_eq!(
9129            scene_delta.new_graph.objects.len(),
9130            11,
9131            "{:#?}",
9132            scene_delta.new_graph.objects
9133        );
9134
9135        ctx.close().await;
9136        mock_ctx.close().await;
9137    }
9138
9139    #[tokio::test(flavor = "multi_thread")]
9140    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9141        let initial_source = "\
9142sketch(on = XY) {
9143  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9144  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9145  fixed([line1.start, [0, 0]])
9146  coincident([line1.end, line2.start])
9147  equalLength([line1, line2])
9148}
9149";
9150
9151        let program = Program::parse(initial_source).unwrap().0.unwrap();
9152        let mut frontend = FrontendState::new();
9153        let mock_ctx = ExecutorContext::new_mock(None).await;
9154        let version = Version(0);
9155
9156        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9157        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9158        let sketch_id = sketch_object.id;
9159        let sketch = expect_sketch(sketch_object);
9160        let line2_end_id = *sketch.segments.get(4).unwrap();
9161
9162        let segments = vec![ExistingSegmentCtor {
9163            id: line2_end_id,
9164            ctor: SegmentCtor::Point(PointCtor {
9165                position: Point2d {
9166                    x: Expr::Var(Number {
9167                        value: 9.0,
9168                        units: NumericSuffix::None,
9169                    }),
9170                    y: Expr::Var(Number {
9171                        value: 10.0,
9172                        units: NumericSuffix::None,
9173                    }),
9174                },
9175            }),
9176        }];
9177        let (edited_source, _) = frontend
9178            .edit_segments(&mock_ctx, version, sketch_id, segments)
9179            .await
9180            .unwrap();
9181
9182        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9183        assert_eq!(mock_source.text, edited_source.text);
9184
9185        mock_ctx.close().await;
9186    }
9187
9188    /// Preview segment edits should return solved geometry without persisting
9189    /// solver feedback to KCL.
9190    #[tokio::test(flavor = "multi_thread")]
9191    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9192        let initial_source = "\
9193sketch(on = XY) {
9194  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9195  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9196  fixed([line1.start, [0, 0]])
9197  coincident([line1.end, line2.start])
9198  equalLength([line1, line2])
9199}
9200";
9201
9202        let program = Program::parse(initial_source).unwrap().0.unwrap();
9203        let mut frontend = FrontendState::new();
9204        let mock_ctx = ExecutorContext::new_mock(None).await;
9205        let version = Version(0);
9206
9207        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9208        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209        let sketch_id = sketch_object.id;
9210        let sketch = expect_sketch(sketch_object);
9211        let line2_end_id = *sketch.segments.get(4).unwrap();
9212
9213        let segments = vec![ExistingSegmentCtor {
9214            id: line2_end_id,
9215            ctor: SegmentCtor::Point(PointCtor {
9216                position: Point2d {
9217                    x: Expr::Var(Number {
9218                        value: 9.0,
9219                        units: NumericSuffix::None,
9220                    }),
9221                    y: Expr::Var(Number {
9222                        value: 10.0,
9223                        units: NumericSuffix::None,
9224                    }),
9225                },
9226            }),
9227        }];
9228        let (preview_source, preview_delta) = frontend
9229            .edit_segments_with_options(
9230                &mock_ctx,
9231                version,
9232                sketch_id,
9233                segments,
9234                EditSegmentsOptions {
9235                    anchor_segment_ids: Some(vec![line2_end_id]),
9236                    drag_anchors: Vec::new(),
9237                    constraint_label_edits: Vec::new(),
9238                    commit_solved_initial_guesses: false,
9239                },
9240            )
9241            .await
9242            .unwrap();
9243
9244        assert!(
9245            !preview_delta.exec_outcome.var_solutions.is_empty(),
9246            "preview solve should still solve and return geometry feedback"
9247        );
9248        assert!(
9249            preview_source
9250                .text
9251                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9252        );
9253        assert!(
9254            preview_source
9255                .text
9256                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9257        );
9258
9259        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9260        assert_eq!(mock_source.text, preview_source.text);
9261
9262        mock_ctx.close().await;
9263    }
9264
9265    #[tokio::test(flavor = "multi_thread")]
9266    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9267        let initial_source = "\
9268sketch(on = XY) {
9269  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9270}
9271";
9272
9273        let program = Program::parse(initial_source).unwrap().0.unwrap();
9274        let mut frontend = FrontendState::new();
9275        let mock_ctx = ExecutorContext::new_mock(None).await;
9276        let version = Version(0);
9277
9278        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9279        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9280        let sketch_id = sketch_object.id;
9281        let sketch = expect_sketch(sketch_object);
9282        let line_end_id = *sketch.segments.get(1).unwrap();
9283
9284        let constraint = Constraint::Fixed(Fixed {
9285            points: vec![FixedPoint {
9286                point: line_end_id,
9287                position: Point2d {
9288                    x: Number {
9289                        value: 20.0,
9290                        units: NumericSuffix::Mm,
9291                    },
9292                    y: Number {
9293                        value: 0.0,
9294                        units: NumericSuffix::Mm,
9295                    },
9296                },
9297            }],
9298        });
9299        let (constraint_source, _) = frontend
9300            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9301            .await
9302            .unwrap();
9303
9304        assert!(
9305            constraint_source
9306                .text
9307                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9308            "{}",
9309            constraint_source.text
9310        );
9311        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9312        assert_eq!(mock_source.text, constraint_source.text);
9313
9314        mock_ctx.close().await;
9315    }
9316
9317    #[test]
9318    fn test_no_solver_feedback_preserves_original_source() {
9319        let initial_source = "\
9320@settings(defaultLengthUnit = in, kclVersion = 2.0)
9321cylinder = startSketchOn(XY)
9322    |> circle(center= [0, 0], radius= 22)
9323    |> extrude(length = 14)
9324";
9325        let mut frontend = FrontendState::new();
9326        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9327        let outcome = ExecOutcome {
9328            variables: Default::default(),
9329            test_program_memory: Default::default(),
9330            operations: Default::default(),
9331            artifact_graph: Default::default(),
9332            scene_objects: Default::default(),
9333            source_range_to_object: Default::default(),
9334            var_solutions: Default::default(),
9335            refactor_metadata: Default::default(),
9336            issues: Default::default(),
9337            filenames: Default::default(),
9338            engine_scene_generation: 0,
9339            source_files: Default::default(),
9340            default_planes: Default::default(),
9341        };
9342
9343        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9344
9345        assert_eq!(source_delta.text, initial_source);
9346    }
9347
9348    /// Explicit drag anchors should limit which edited points become temporary
9349    /// fixed constraints.
9350    #[tokio::test(flavor = "multi_thread")]
9351    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9352        let initial_source = "\
9353sketch(on = XY) {
9354  point1 = point(at = [var 0mm, var 0mm])
9355  point2 = point(at = [var 0mm, var 0mm])
9356  coincident([point1, point2])
9357}
9358";
9359
9360        let program = Program::parse(initial_source).unwrap().0.unwrap();
9361        let mut frontend = FrontendState::new();
9362        let mock_ctx = ExecutorContext::new_mock(None).await;
9363        let version = Version(0);
9364
9365        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9366        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9367        let sketch_id = sketch_object.id;
9368        let sketch = expect_sketch(sketch_object);
9369        let point1_id = sketch.segments[0];
9370        let point2_id = sketch.segments[1];
9371
9372        let segments = vec![
9373            ExistingSegmentCtor {
9374                id: point1_id,
9375                ctor: SegmentCtor::Point(PointCtor {
9376                    position: point_expr_mm(10.0, 0.0),
9377                }),
9378            },
9379            ExistingSegmentCtor {
9380                id: point2_id,
9381                ctor: SegmentCtor::Point(PointCtor {
9382                    position: point_expr_mm(100.0, 0.0),
9383                }),
9384            },
9385        ];
9386        let (_, scene_delta) = frontend
9387            .edit_segments_with_options(
9388                &mock_ctx,
9389                version,
9390                sketch_id,
9391                segments,
9392                EditSegmentsOptions {
9393                    anchor_segment_ids: Some(vec![point1_id]),
9394                    drag_anchors: Vec::new(),
9395                    constraint_label_edits: Vec::new(),
9396                    commit_solved_initial_guesses: true,
9397                },
9398            )
9399            .await
9400            .unwrap();
9401
9402        assert_point_position_close(
9403            point_position(&scene_delta.new_graph, point1_id),
9404            point_number_mm(10.0, 0.0),
9405        );
9406        assert_point_position_close(
9407            point_position(&scene_delta.new_graph, point2_id),
9408            point_number_mm(10.0, 0.0),
9409        );
9410
9411        mock_ctx.close().await;
9412    }
9413
9414    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9415    /// for every SketchVar whose initial NumericLiteral has the given value.
9416    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9417        use std::cell::RefCell;
9418        struct Collector {
9419            target: f64,
9420            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9421        }
9422        impl<'a> crate::walk::Visitor<'a> for &Collector {
9423            type Error = crate::front::Error;
9424            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9425                if let crate::walk::Node::SketchVar(sketch_var) = node
9426                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9427                    && (initial.value - self.target).abs() < 1e-9
9428                {
9429                    self.out
9430                        .borrow_mut()
9431                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9432                }
9433                for child in node.children().iter() {
9434                    if !child.visit(*self)? {
9435                        return Ok(false);
9436                    }
9437                }
9438                Ok(true)
9439            }
9440        }
9441        let collector = Collector {
9442            target: value,
9443            out: Default::default(),
9444        };
9445        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9446        collector.out.into_inner()
9447    }
9448
9449    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9450    /// for every SketchVar (including bare `var`).
9451    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9452        use std::cell::RefCell;
9453        struct Collector {
9454            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9455        }
9456        impl<'a> crate::walk::Visitor<'a> for &Collector {
9457            type Error = crate::front::Error;
9458            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9459                if let crate::walk::Node::SketchVar(sketch_var) = node
9460                    && let Some(node_path) = &sketch_var.node_path
9461                {
9462                    self.out
9463                        .borrow_mut()
9464                        .push((SourceRange::from(sketch_var), node_path.clone()));
9465                }
9466                for child in node.children().iter() {
9467                    if !child.visit(*self)? {
9468                        return Ok(false);
9469                    }
9470                }
9471                Ok(true)
9472            }
9473        }
9474        let collector = Collector {
9475            out: Default::default(),
9476        };
9477        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9478        collector.out.into_inner()
9479    }
9480
9481    fn empty_exec_outcome_with_var_solutions(
9482        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9483    ) -> ExecOutcome {
9484        ExecOutcome {
9485            variables: Default::default(),
9486            test_program_memory: Default::default(),
9487            operations: Default::default(),
9488            artifact_graph: Default::default(),
9489            scene_objects: Default::default(),
9490            source_range_to_object: Default::default(),
9491            var_solutions,
9492            refactor_metadata: Default::default(),
9493            issues: Default::default(),
9494            filenames: Default::default(),
9495            engine_scene_generation: 0,
9496            source_files: Default::default(),
9497            default_planes: Default::default(),
9498        }
9499    }
9500
9501    /// Happy path: commit a var solution to a `var N` inside a sketch block
9502    /// using a correct NodePath. Confirms the node-path code path produces the
9503    /// expected source mutation.
9504    #[test]
9505    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9506        let initial_source = "\
9507sketch(on = XY) {
9508  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9509}
9510";
9511        let program = Program::parse(initial_source).unwrap().0.unwrap();
9512        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9513        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9514        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9515
9516        let mut frontend = FrontendState::new();
9517        frontend.program = program;
9518
9519        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9520            literal_range,
9521            Some(node_path),
9522            Number {
9523                value: 25.0,
9524                units: NumericSuffix::Mm,
9525            },
9526        )]);
9527
9528        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9529
9530        insta::assert_snapshot!(
9531            "test_commit_var_solution_by_node_path_updates_sketch_var",
9532            source_delta.text
9533        );
9534    }
9535
9536    /// Whitespace inserted earlier in the source shifts the original SketchVar
9537    /// SourceRange. With NodePath propagation the commit should still target
9538    /// the right `var`. We simulate this by collecting node_paths against a
9539    /// "compact" source, then loading the frontend with a "padded" source
9540    /// (whose byte offsets differ), and feeding the original (now stale)
9541    /// source range plus the correct node_path back into the commit.
9542    #[test]
9543    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9544        let compact_source = "\
9545sketch(on = XY) {
9546  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9547}
9548";
9549        let padded_source = "\
9550// added comment\n// added comment\n\nsketch(on = XY) {
9551  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9552}
9553";
9554        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9555        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9556
9557        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9558            .into_iter()
9559            .next()
9560            .expect("expected `var 10mm` in compact source");
9561        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9562            .into_iter()
9563            .next()
9564            .expect("expected `var 10mm` in padded source");
9565
9566        assert_ne!(
9567            compact_match.0, padded_match.0,
9568            "byte offsets must differ for this test to be meaningful"
9569        );
9570        assert_eq!(
9571            compact_match.1, padded_match.1,
9572            "node paths must agree across whitespace; that's the whole point of NodePath",
9573        );
9574
9575        let mut frontend = FrontendState::new();
9576        frontend.program = padded_program;
9577
9578        // Stale source range from the compact source + correct node_path.
9579        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9580            compact_match.0,
9581            Some(compact_match.1),
9582            Number {
9583                value: 30.0,
9584                units: NumericSuffix::Mm,
9585            },
9586        )]);
9587
9588        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9589
9590        insta::assert_snapshot!(
9591            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9592            source_delta.text
9593        );
9594    }
9595
9596    /// When multiple `var` declarations exist and the stale source range
9597    /// happens to land on a *different* var, the node_path must take
9598    /// precedence and the right var gets updated.
9599    #[test]
9600    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9601        let initial_source = "\
9602sketch(on = XY) {
9603  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9604}
9605";
9606        let program = Program::parse(initial_source).unwrap().0.unwrap();
9607
9608        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9609            .into_iter()
9610            .next()
9611            .expect("expected `var 10mm`");
9612        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9613            .into_iter()
9614            .next()
9615            .expect("expected `var 20mm`");
9616
9617        let mut frontend = FrontendState::new();
9618        frontend.program = program;
9619
9620        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9621        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9622            var_20.0,
9623            Some(var_10.1),
9624            Number {
9625                value: 33.0,
9626                units: NumericSuffix::Mm,
9627            },
9628        )]);
9629
9630        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9631
9632        insta::assert_snapshot!(
9633            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9634            source_delta.text
9635        );
9636    }
9637
9638    /// Bare `var` (no initial literal) is only locatable via node_path. With
9639    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9640    /// solver solution should fill the initial value in. The
9641    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9642    /// is gated as an experimental feature; without it the re-parse of the
9643    /// recast source rejects bare `var` declarations.
9644    #[test]
9645    fn test_commit_var_solution_writes_back_into_bare_var() {
9646        let initial_source = "\
9647@settings(experimentalFeatures = allow, kclVersion = 2.0)
9648sketch(on = XY) {
9649  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9650}
9651";
9652        let program = Program::parse(initial_source).unwrap().0.unwrap();
9653
9654        // Pick the first bare `var`; collect_all_sketch_vars returns every
9655        // SketchVar, including bare ones.
9656        let bare = collect_all_sketch_vars(&program)
9657            .into_iter()
9658            .find(|(range, _)| {
9659                // The bare `var` is exactly the 3 characters "var".
9660                range.end() - range.start() == 3
9661            })
9662            .expect("expected at least one bare `var`");
9663
9664        let mut frontend = FrontendState::new();
9665        frontend.program = program;
9666
9667        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9668            bare.0,
9669            Some(bare.1),
9670            Number {
9671                value: 7.0,
9672                units: NumericSuffix::Mm,
9673            },
9674        )]);
9675
9676        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9677
9678        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9679        // written as an explicit suffix so the bare var commits with units.
9680        // The recast adds a blank line after the `@settings` annotation.
9681        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9682    }
9683
9684    #[tokio::test(flavor = "multi_thread")]
9685    async fn test_delete_point_without_var() {
9686        let initial_source = "\
9687sketch(on = XY) {
9688  point(at = [var 1, var 2])
9689  point(at = [var 3, var 4])
9690  point(at = [var 5, var 6])
9691}
9692";
9693
9694        let program = Program::parse(initial_source).unwrap().0.unwrap();
9695
9696        let mut frontend = FrontendState::new();
9697
9698        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9699        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9700        let version = Version(0);
9701
9702        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9703        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9704        let sketch_id = sketch_object.id;
9705        let sketch = expect_sketch(sketch_object);
9706
9707        let point_id = *sketch.segments.get(1).unwrap();
9708
9709        let (src_delta, scene_delta) = frontend
9710            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9711            .await
9712            .unwrap();
9713        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9714        assert_eq!(scene_delta.new_objects, vec![]);
9715        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9716
9717        ctx.close().await;
9718        mock_ctx.close().await;
9719    }
9720
9721    #[tokio::test(flavor = "multi_thread")]
9722    async fn test_delete_point_with_var() {
9723        let initial_source = "\
9724sketch(on = XY) {
9725  point(at = [var 1, var 2])
9726  point1 = point(at = [var 3, var 4])
9727  point(at = [var 5, var 6])
9728}
9729";
9730
9731        let program = Program::parse(initial_source).unwrap().0.unwrap();
9732
9733        let mut frontend = FrontendState::new();
9734
9735        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9736        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9737        let version = Version(0);
9738
9739        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9740        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9741        let sketch_id = sketch_object.id;
9742        let sketch = expect_sketch(sketch_object);
9743
9744        let point_id = *sketch.segments.get(1).unwrap();
9745
9746        let (src_delta, scene_delta) = frontend
9747            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9748            .await
9749            .unwrap();
9750        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9751        assert_eq!(scene_delta.new_objects, vec![]);
9752        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9753
9754        ctx.close().await;
9755        mock_ctx.close().await;
9756    }
9757
9758    #[tokio::test(flavor = "multi_thread")]
9759    async fn test_delete_multiple_points() {
9760        let initial_source = "\
9761sketch(on = XY) {
9762  point(at = [var 1, var 2])
9763  point1 = point(at = [var 3, var 4])
9764  point(at = [var 5, var 6])
9765}
9766";
9767
9768        let program = Program::parse(initial_source).unwrap().0.unwrap();
9769
9770        let mut frontend = FrontendState::new();
9771
9772        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9773        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9774        let version = Version(0);
9775
9776        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9777        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9778        let sketch_id = sketch_object.id;
9779
9780        let sketch = expect_sketch(sketch_object);
9781
9782        let point1_id = *sketch.segments.first().unwrap();
9783        let point2_id = *sketch.segments.get(1).unwrap();
9784
9785        let (src_delta, scene_delta) = frontend
9786            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9787            .await
9788            .unwrap();
9789        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9790        assert_eq!(scene_delta.new_objects, vec![]);
9791        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9792
9793        ctx.close().await;
9794        mock_ctx.close().await;
9795    }
9796
9797    #[tokio::test(flavor = "multi_thread")]
9798    async fn test_delete_coincident_constraint() {
9799        let initial_source = "\
9800sketch(on = XY) {
9801  point1 = point(at = [var 1, var 2])
9802  point2 = point(at = [var 3, var 4])
9803  coincident([point1, point2])
9804  point(at = [var 5, var 6])
9805}
9806";
9807
9808        let program = Program::parse(initial_source).unwrap().0.unwrap();
9809
9810        let mut frontend = FrontendState::new();
9811
9812        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9813        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9814        let version = Version(0);
9815
9816        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9817        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9818        let sketch_id = sketch_object.id;
9819        let sketch = expect_sketch(sketch_object);
9820
9821        let coincident_id = *sketch.constraints.first().unwrap();
9822
9823        let (src_delta, scene_delta) = frontend
9824            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9825            .await
9826            .unwrap();
9827        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9828        assert_eq!(scene_delta.new_objects, vec![]);
9829        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9830
9831        ctx.close().await;
9832        mock_ctx.close().await;
9833    }
9834
9835    #[tokio::test(flavor = "multi_thread")]
9836    async fn test_delete_line_cascades_to_coincident_constraint() {
9837        let initial_source = "\
9838sketch(on = XY) {
9839  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9840  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9841  coincident([line1.end, line2.start])
9842}
9843";
9844
9845        let program = Program::parse(initial_source).unwrap().0.unwrap();
9846
9847        let mut frontend = FrontendState::new();
9848
9849        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9850        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9851        let version = Version(0);
9852
9853        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9854        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9855        let sketch_id = sketch_object.id;
9856        let sketch = expect_sketch(sketch_object);
9857        let line_id = *sketch.segments.get(5).unwrap();
9858
9859        let (src_delta, scene_delta) = frontend
9860            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9861            .await
9862            .unwrap();
9863        insta::assert_snapshot!(
9864            "test_delete_line_cascades_to_coincident_constraint",
9865            src_delta.text.as_str()
9866        );
9867        assert_eq!(
9868            scene_delta.new_graph.objects.len(),
9869            5,
9870            "{:#?}",
9871            scene_delta.new_graph.objects
9872        );
9873
9874        ctx.close().await;
9875        mock_ctx.close().await;
9876    }
9877
9878    #[tokio::test(flavor = "multi_thread")]
9879    async fn test_delete_line_cascades_to_distance_constraint() {
9880        let initial_source = "\
9881sketch(on = XY) {
9882  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9883  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9884  distance([line1.end, line2.start]) == 10mm
9885}
9886";
9887
9888        let program = Program::parse(initial_source).unwrap().0.unwrap();
9889
9890        let mut frontend = FrontendState::new();
9891
9892        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9893        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
9894        let version = Version(0);
9895
9896        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9897        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9898        let sketch_id = sketch_object.id;
9899        let sketch = expect_sketch(sketch_object);
9900        let line_id = *sketch.segments.get(5).unwrap();
9901
9902        let (src_delta, scene_delta) = frontend
9903            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9904            .await
9905            .unwrap();
9906        insta::assert_snapshot!(
9907            "test_delete_line_cascades_to_distance_constraint",
9908            src_delta.text.as_str()
9909        );
9910        assert_eq!(
9911            scene_delta.new_graph.objects.len(),
9912            5,
9913            "{:#?}",
9914            scene_delta.new_graph.objects
9915        );
9916
9917        ctx.close().await;
9918        mock_ctx.close().await;
9919    }
9920
9921    #[tokio::test(flavor = "multi_thread")]
9922    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9923        let initial_source = "\
9924sketch(on = XY) {
9925  point1 = point(at = [var 1, var 2])
9926  point2 = point(at = [var 3, var 4])
9927  horizontalDistance([point1, point2]) == 10mm
9928}
9929";
9930
9931        let program = Program::parse(initial_source).unwrap().0.unwrap();
9932
9933        let mut frontend = FrontendState::new();
9934
9935        let mock_ctx = ExecutorContext::new_mock(None).await;
9936        let version = Version(0);
9937
9938        frontend.program = program.clone();
9939        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9940        frontend.update_state_after_exec(outcome, true);
9941        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9942        let sketch_id = sketch_object.id;
9943        let sketch = expect_sketch(sketch_object);
9944        let point2_id = *sketch.segments.get(1).unwrap();
9945
9946        let (src_delta, scene_delta) = frontend
9947            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9948            .await
9949            .unwrap();
9950        insta::assert_snapshot!(
9951            "test_delete_point_cascades_to_horizontal_distance_constraint",
9952            src_delta.text.as_str()
9953        );
9954        assert_eq!(
9955            scene_delta.new_graph.objects.len(),
9956            3,
9957            "{:#?}",
9958            scene_delta.new_graph.objects
9959        );
9960
9961        mock_ctx.close().await;
9962    }
9963
9964    #[tokio::test(flavor = "multi_thread")]
9965    async fn test_delete_line_cascades_to_fixed_constraint() {
9966        let initial_source = "\
9967sketch(on = XY) {
9968  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9969  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9970  fixed([line1.start, [0, 0]])
9971}
9972";
9973
9974        let program = Program::parse(initial_source).unwrap().0.unwrap();
9975
9976        let mut frontend = FrontendState::new();
9977
9978        let mock_ctx = ExecutorContext::new_mock(None).await;
9979        let version = Version(0);
9980
9981        frontend.program = program.clone();
9982        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9983        frontend.update_state_after_exec(outcome, true);
9984        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9985        let sketch_id = sketch_object.id;
9986        let sketch = expect_sketch(sketch_object);
9987        let line1_id = *sketch.segments.get(2).unwrap();
9988
9989        let (src_delta, scene_delta) = frontend
9990            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9991            .await
9992            .unwrap();
9993        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9994        assert_eq!(
9995            scene_delta.new_graph.objects.len(),
9996            5,
9997            "{:#?}",
9998            scene_delta.new_graph.objects
9999        );
10000
10001        mock_ctx.close().await;
10002    }
10003
10004    #[tokio::test(flavor = "multi_thread")]
10005    async fn test_delete_line_cascades_to_midpoint_constraint() {
10006        let initial_source = "\
10007sketch(on = XY) {
10008  point1 = point(at = [var 1, var 2])
10009  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10010  midpoint(line1, point = point1)
10011}
10012";
10013
10014        let program = Program::parse(initial_source).unwrap().0.unwrap();
10015
10016        let mut frontend = FrontendState::new();
10017
10018        let mock_ctx = ExecutorContext::new_mock(None).await;
10019        let version = Version(0);
10020
10021        frontend.program = program.clone();
10022        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10023        frontend.update_state_after_exec(outcome, true);
10024        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10025        let sketch_id = sketch_object.id;
10026        let sketch = expect_sketch(sketch_object);
10027        let line1_id = *sketch.segments.get(3).unwrap();
10028
10029        let (src_delta, scene_delta) = frontend
10030            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10031            .await
10032            .unwrap();
10033        insta::assert_snapshot!(
10034            "test_delete_line_cascades_to_midpoint_constraint",
10035            src_delta.text.as_str()
10036        );
10037        assert_eq!(
10038            scene_delta.new_graph.objects.len(),
10039            3,
10040            "{:#?}",
10041            scene_delta.new_graph.objects
10042        );
10043
10044        mock_ctx.close().await;
10045    }
10046
10047    #[tokio::test(flavor = "multi_thread")]
10048    async fn test_delete_point_preserves_multiline_coincident_constraint() {
10049        let initial_source = "\
10050sketch(on = XY) {
10051  point1 = point(at = [var 1, var 2])
10052  point2 = point(at = [var 3, var 4])
10053  point3 = point(at = [var 5, var 6])
10054  coincident([point1, point2, point3])
10055}
10056";
10057
10058        let program = Program::parse(initial_source).unwrap().0.unwrap();
10059
10060        let mut frontend = FrontendState::new();
10061
10062        let mock_ctx = ExecutorContext::new_mock(None).await;
10063        let version = Version(0);
10064
10065        frontend.program = program.clone();
10066        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10067        frontend.update_state_after_exec(outcome, true);
10068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10069        let sketch_id = sketch_object.id;
10070        let sketch = expect_sketch(sketch_object);
10071        let point3_id = *sketch.segments.get(2).unwrap();
10072
10073        let (src_delta, scene_delta) = frontend
10074            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10075            .await
10076            .unwrap();
10077        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10078        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10079        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10080        assert!(
10081            src_delta.text.contains("coincident([point1, point2])"),
10082            "{}",
10083            src_delta.text
10084        );
10085
10086        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10087        let sketch = expect_sketch(sketch_object);
10088        assert_eq!(sketch.segments.len(), 2);
10089        assert_eq!(sketch.constraints.len(), 1);
10090
10091        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10092        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10093            panic!("Expected constraint object");
10094        };
10095        let Constraint::Coincident(coincident) = constraint else {
10096            panic!("Expected coincident constraint");
10097        };
10098        assert_eq!(
10099            coincident.segments,
10100            sketch
10101                .segments
10102                .iter()
10103                .copied()
10104                .map(Into::into)
10105                .collect::<Vec<ConstraintSegment>>()
10106        );
10107
10108        mock_ctx.close().await;
10109    }
10110
10111    #[tokio::test(flavor = "multi_thread")]
10112    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10113        let initial_source = "\
10114sketch(on = XY) {
10115  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10116  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10117  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10118  equalLength([line1, line2, line3])
10119}
10120";
10121
10122        let program = Program::parse(initial_source).unwrap().0.unwrap();
10123
10124        let mut frontend = FrontendState::new();
10125
10126        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10127        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10128        let version = Version(0);
10129
10130        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10131        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10132        let sketch_id = sketch_object.id;
10133        let sketch = expect_sketch(sketch_object);
10134        let line3_id = *sketch.segments.get(8).unwrap();
10135
10136        let (src_delta, scene_delta) = frontend
10137            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10138            .await
10139            .unwrap();
10140        insta::assert_snapshot!(
10141            "test_delete_line_preserves_multiline_equal_length_constraint",
10142            src_delta.text.as_str()
10143        );
10144
10145        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10146        let sketch = expect_sketch(sketch_object);
10147        assert_eq!(sketch.constraints.len(), 1);
10148
10149        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10150        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10151            panic!("Expected constraint object");
10152        };
10153        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10154            panic!("Expected lines equal length constraint");
10155        };
10156        assert_eq!(lines_equal_length.lines.len(), 2);
10157
10158        ctx.close().await;
10159        mock_ctx.close().await;
10160    }
10161
10162    #[tokio::test(flavor = "multi_thread")]
10163    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10164        let initial_source = "\
10165sketch(on = XY) {
10166  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10167  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10168  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10169  horizontal([line1.end, line2.start, line3.start])
10170}
10171";
10172
10173        let program = Program::parse(initial_source).unwrap().0.unwrap();
10174
10175        let mut frontend = FrontendState::new();
10176
10177        let mock_ctx = ExecutorContext::new_mock(None).await;
10178        let version = Version(0);
10179
10180        frontend.program = program.clone();
10181        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10182        frontend.update_state_after_exec(outcome, true);
10183        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10184        let sketch_id = sketch_object.id;
10185        let sketch = expect_sketch(sketch_object);
10186        let line1_id = *sketch.segments.get(2).unwrap();
10187
10188        let (src_delta, scene_delta) = frontend
10189            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10190            .await
10191            .unwrap();
10192        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10193        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10194        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10195        assert!(
10196            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10197            "{}",
10198            src_delta.text
10199        );
10200
10201        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10202        let sketch = expect_sketch(sketch_object);
10203        assert_eq!(sketch.constraints.len(), 1);
10204
10205        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10206        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10207            panic!("Expected constraint object");
10208        };
10209        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10210            panic!("Expected horizontal points constraint");
10211        };
10212        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10213        assert_eq!(*points, remaining_points);
10214
10215        mock_ctx.close().await;
10216    }
10217
10218    #[tokio::test(flavor = "multi_thread")]
10219    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10220        let initial_source = "\
10221sketch(on = XY) {
10222  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10223  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10224  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10225  vertical([line1.end, line2.start, line3.start])
10226}
10227";
10228
10229        let program = Program::parse(initial_source).unwrap().0.unwrap();
10230
10231        let mut frontend = FrontendState::new();
10232
10233        let mock_ctx = ExecutorContext::new_mock(None).await;
10234        let version = Version(0);
10235
10236        frontend.program = program.clone();
10237        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10238        frontend.update_state_after_exec(outcome, true);
10239        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10240        let sketch_id = sketch_object.id;
10241        let sketch = expect_sketch(sketch_object);
10242        let line1_id = *sketch.segments.get(2).unwrap();
10243
10244        let (src_delta, scene_delta) = frontend
10245            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10246            .await
10247            .unwrap();
10248        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10249        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10250        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10251        assert!(
10252            src_delta.text.contains("vertical([line2.start, line3.start])"),
10253            "{}",
10254            src_delta.text
10255        );
10256
10257        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10258        let sketch = expect_sketch(sketch_object);
10259        assert_eq!(sketch.constraints.len(), 1);
10260
10261        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10262        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10263            panic!("Expected constraint object");
10264        };
10265        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10266            panic!("Expected vertical points constraint");
10267        };
10268        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10269        assert_eq!(*points, remaining_points);
10270
10271        mock_ctx.close().await;
10272    }
10273
10274    #[tokio::test(flavor = "multi_thread")]
10275    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10276        let initial_source = "\
10277sketch(on = XY) {
10278  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10279  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10280  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10281  coincident([line1.end, line2.start, line3.start])
10282}
10283";
10284
10285        let program = Program::parse(initial_source).unwrap().0.unwrap();
10286
10287        let mut frontend = FrontendState::new();
10288
10289        let mock_ctx = ExecutorContext::new_mock(None).await;
10290        let version = Version(0);
10291
10292        frontend.program = program.clone();
10293        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10294        frontend.update_state_after_exec(outcome, true);
10295        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10296        let sketch_id = sketch_object.id;
10297        let sketch = expect_sketch(sketch_object);
10298        let line1_id = *sketch.segments.get(2).unwrap();
10299
10300        let (src_delta, scene_delta) = frontend
10301            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10302            .await
10303            .unwrap();
10304        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10305        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10306        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10307        assert!(
10308            src_delta.text.contains("coincident([line2.start, line3.start])"),
10309            "{}",
10310            src_delta.text
10311        );
10312
10313        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10314        let sketch = expect_sketch(sketch_object);
10315        assert_eq!(sketch.constraints.len(), 1);
10316
10317        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10318        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10319            panic!("Expected constraint object");
10320        };
10321        let Constraint::Coincident(coincident) = constraint else {
10322            panic!("Expected coincident constraint");
10323        };
10324        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10325        assert_eq!(coincident.segments, remaining_segments);
10326
10327        mock_ctx.close().await;
10328    }
10329
10330    #[tokio::test(flavor = "multi_thread")]
10331    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10332        let initial_source = "\
10333sketch(on = XY) {
10334  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10335  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10336  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10337  equalLength([line1, line2, line3])
10338}
10339";
10340
10341        let program = Program::parse(initial_source).unwrap().0.unwrap();
10342
10343        let mut frontend = FrontendState::new();
10344
10345        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10346        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10347        let version = Version(0);
10348
10349        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10350        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10351        let sketch_id = sketch_object.id;
10352        let sketch = expect_sketch(sketch_object);
10353        let line2_id = *sketch.segments.get(5).unwrap();
10354        let line3_id = *sketch.segments.get(8).unwrap();
10355
10356        let (src_delta, scene_delta) = frontend
10357            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10358            .await
10359            .unwrap();
10360        insta::assert_snapshot!(
10361            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10362            src_delta.text.as_str()
10363        );
10364
10365        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10366        let sketch = expect_sketch(sketch_object);
10367        assert!(sketch.constraints.is_empty());
10368
10369        ctx.close().await;
10370        mock_ctx.close().await;
10371    }
10372
10373    #[tokio::test(flavor = "multi_thread")]
10374    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10375        let initial_source = "\
10376sketch(on = XY) {
10377  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10378  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10379  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10380  parallel([line1, line2, line3])
10381}
10382";
10383
10384        let program = Program::parse(initial_source).unwrap().0.unwrap();
10385
10386        let mut frontend = FrontendState::new();
10387
10388        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10389        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10390        let version = Version(0);
10391
10392        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10393        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10394        let sketch_id = sketch_object.id;
10395        let sketch = expect_sketch(sketch_object);
10396        let line3_id = *sketch.segments.get(8).unwrap();
10397
10398        let (src_delta, scene_delta) = frontend
10399            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10400            .await
10401            .unwrap();
10402        insta::assert_snapshot!(
10403            "test_delete_line_preserves_multiline_parallel_constraint",
10404            src_delta.text.as_str()
10405        );
10406
10407        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10408        let sketch = expect_sketch(sketch_object);
10409        assert_eq!(sketch.constraints.len(), 1);
10410
10411        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10412        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10413            panic!("Expected constraint object");
10414        };
10415        let Constraint::Parallel(parallel) = constraint else {
10416            panic!("Expected parallel constraint");
10417        };
10418        assert_eq!(parallel.lines.len(), 2);
10419
10420        ctx.close().await;
10421        mock_ctx.close().await;
10422    }
10423
10424    #[tokio::test(flavor = "multi_thread")]
10425    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10426        let initial_source = "\
10427sketch(on = XY) {
10428  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10429  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10430  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10431  parallel([line1, line2, line3])
10432}
10433";
10434
10435        let program = Program::parse(initial_source).unwrap().0.unwrap();
10436
10437        let mut frontend = FrontendState::new();
10438
10439        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10440        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10441        let version = Version(0);
10442
10443        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10444        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10445        let sketch_id = sketch_object.id;
10446        let sketch = expect_sketch(sketch_object);
10447        let line2_id = *sketch.segments.get(5).unwrap();
10448        let line3_id = *sketch.segments.get(8).unwrap();
10449
10450        let (src_delta, scene_delta) = frontend
10451            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10452            .await
10453            .unwrap();
10454        insta::assert_snapshot!(
10455            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10456            src_delta.text.as_str()
10457        );
10458
10459        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10460        let sketch = expect_sketch(sketch_object);
10461        assert!(sketch.constraints.is_empty());
10462
10463        ctx.close().await;
10464        mock_ctx.close().await;
10465    }
10466
10467    #[tokio::test(flavor = "multi_thread")]
10468    async fn test_delete_line_line_coincident_constraint() {
10469        let initial_source = "\
10470sketch(on = XY) {
10471  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10472  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10473  coincident([line1, line2])
10474}
10475";
10476
10477        let program = Program::parse(initial_source).unwrap().0.unwrap();
10478
10479        let mut frontend = FrontendState::new();
10480
10481        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10482        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10483        let version = Version(0);
10484
10485        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10486        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10487        let sketch_id = sketch_object.id;
10488        let sketch = expect_sketch(sketch_object);
10489
10490        let coincident_id = *sketch.constraints.first().unwrap();
10491
10492        let (src_delta, scene_delta) = frontend
10493            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10494            .await
10495            .unwrap();
10496        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10497        assert_eq!(scene_delta.new_objects, vec![]);
10498        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10499
10500        ctx.close().await;
10501        mock_ctx.close().await;
10502    }
10503
10504    #[tokio::test(flavor = "multi_thread")]
10505    async fn test_two_points_coincident() {
10506        let initial_source = "\
10507sketch(on = XY) {
10508  point1 = point(at = [var 1, var 2])
10509  point(at = [3, 4])
10510}
10511";
10512
10513        let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515        let mut frontend = FrontendState::new();
10516
10517        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10518        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10519        let version = Version(0);
10520
10521        frontend.hack_set_program(&ctx, program).await.unwrap();
10522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523        let sketch_id = sketch_object.id;
10524        let sketch = expect_sketch(sketch_object);
10525        let point0_id = *sketch.segments.first().unwrap();
10526        let point1_id = *sketch.segments.get(1).unwrap();
10527
10528        let constraint = Constraint::Coincident(Coincident {
10529            segments: vec![point0_id.into(), point1_id.into()],
10530        });
10531        let (src_delta, scene_delta) = frontend
10532            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10533            .await
10534            .unwrap();
10535        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10536        assert_eq!(
10537            scene_delta.new_graph.objects.len(),
10538            5,
10539            "{:#?}",
10540            scene_delta.new_graph.objects
10541        );
10542
10543        ctx.close().await;
10544        mock_ctx.close().await;
10545    }
10546
10547    #[tokio::test(flavor = "multi_thread")]
10548    async fn test_three_points_coincident() {
10549        let initial_source = "\
10550sketch(on = XY) {
10551  point1 = point(at = [var 1, var 2])
10552  point(at = [var 3, var 4])
10553  point(at = [var 5, var 6])
10554}
10555";
10556
10557        let program = Program::parse(initial_source).unwrap().0.unwrap();
10558
10559        let mut frontend = FrontendState::new();
10560
10561        let mock_ctx = ExecutorContext::new_mock(None).await;
10562        let version = Version(0);
10563
10564        frontend.program = program.clone();
10565        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10566        frontend.update_state_after_exec(outcome, true);
10567        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10568        let sketch_id = sketch_object.id;
10569        let sketch = expect_sketch(sketch_object);
10570        let segments = sketch
10571            .segments
10572            .iter()
10573            .take(3)
10574            .copied()
10575            .map(Into::into)
10576            .collect::<Vec<ConstraintSegment>>();
10577
10578        let constraint = Constraint::Coincident(Coincident {
10579            segments: segments.clone(),
10580        });
10581        let (src_delta, scene_delta) = frontend
10582            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10583            .await
10584            .unwrap();
10585        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10586
10587        let constraint_object = scene_delta
10588            .new_graph
10589            .objects
10590            .iter()
10591            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10592            .unwrap();
10593
10594        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10595            panic!("expected a constraint object");
10596        };
10597
10598        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10599
10600        mock_ctx.close().await;
10601    }
10602
10603    #[tokio::test(flavor = "multi_thread")]
10604    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10605        let initial_source = "\
10606sketch(on = XY) {
10607  point1 = point(at = [var 1, var 2])
10608  point2 = point(at = [var 3, var 4])
10609  point3 = point(at = [var 5, var 6])
10610  coincident([point1, point2, point3])
10611}
10612";
10613
10614        let program = Program::parse(initial_source).unwrap().0.unwrap();
10615
10616        let mut frontend = FrontendState::new();
10617
10618        let ctx = ExecutorContext::new_mock(None).await;
10619        frontend.program = program.clone();
10620        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10621        frontend.update_state_after_exec(outcome, true);
10622
10623        let constraint_object = frontend
10624            .scene_graph
10625            .objects
10626            .iter()
10627            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10628            .unwrap();
10629        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10630            panic!("expected a constraint object");
10631        };
10632
10633        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10634        let sketch = expect_sketch(sketch_object);
10635        let expected_segments = sketch
10636            .segments
10637            .iter()
10638            .take(3)
10639            .copied()
10640            .map(Into::into)
10641            .collect::<Vec<ConstraintSegment>>();
10642
10643        assert_eq!(
10644            constraint,
10645            &Constraint::Coincident(Coincident {
10646                segments: expected_segments,
10647            })
10648        );
10649
10650        ctx.close().await;
10651    }
10652
10653    #[tokio::test(flavor = "multi_thread")]
10654    async fn test_point_origin_coincident_preserves_order() {
10655        let initial_source = "\
10656sketch(on = XY) {
10657  point(at = [var 1, var 2])
10658}
10659";
10660
10661        for (origin_first, snapshot_name) in [
10662            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10663            (false, "test_point_origin_coincident_preserves_order_point_first"),
10664        ] {
10665            let program = Program::parse(initial_source).unwrap().0.unwrap();
10666
10667            let mut frontend = FrontendState::new();
10668
10669            let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10670            let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10671            let version = Version(0);
10672
10673            frontend.hack_set_program(&ctx, program).await.unwrap();
10674            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10675            let sketch_id = sketch_object.id;
10676            let sketch = expect_sketch(sketch_object);
10677            let point_id = *sketch.segments.first().unwrap();
10678
10679            let segments = if origin_first {
10680                vec![ConstraintSegment::ORIGIN, point_id.into()]
10681            } else {
10682                vec![point_id.into(), ConstraintSegment::ORIGIN]
10683            };
10684            let constraint = Constraint::Coincident(Coincident {
10685                segments: segments.clone(),
10686            });
10687            let (src_delta, scene_delta) = frontend
10688                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10689                .await
10690                .unwrap();
10691            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10692
10693            let constraint_object = scene_delta
10694                .new_graph
10695                .objects
10696                .iter()
10697                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10698                .unwrap();
10699
10700            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10701                panic!("expected a constraint object");
10702            };
10703
10704            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10705
10706            ctx.close().await;
10707            mock_ctx.close().await;
10708        }
10709    }
10710
10711    #[tokio::test(flavor = "multi_thread")]
10712    async fn test_coincident_of_line_end_points() {
10713        let initial_source = "\
10714sketch(on = XY) {
10715  line(start = [var 1, var 2], end = [var 3, var 4])
10716  line(start = [var 5, var 6], end = [var 7, var 8])
10717}
10718";
10719
10720        let program = Program::parse(initial_source).unwrap().0.unwrap();
10721
10722        let mut frontend = FrontendState::new();
10723
10724        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10725        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10726        let version = Version(0);
10727
10728        frontend.hack_set_program(&ctx, program).await.unwrap();
10729        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10730        let sketch_id = sketch_object.id;
10731        let sketch = expect_sketch(sketch_object);
10732        let point0_id = *sketch.segments.get(1).unwrap();
10733        let point1_id = *sketch.segments.get(3).unwrap();
10734
10735        let constraint = Constraint::Coincident(Coincident {
10736            segments: vec![point0_id.into(), point1_id.into()],
10737        });
10738        let (src_delta, scene_delta) = frontend
10739            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10740            .await
10741            .unwrap();
10742        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10743        assert_eq!(
10744            scene_delta.new_graph.objects.len(),
10745            9,
10746            "{:#?}",
10747            scene_delta.new_graph.objects
10748        );
10749
10750        ctx.close().await;
10751        mock_ctx.close().await;
10752    }
10753
10754    #[tokio::test(flavor = "multi_thread")]
10755    async fn test_coincident_of_line_point_and_circle_segment() {
10756        let initial_source = "\
10757sketch(on = XY) {
10758  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10759  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10760}
10761";
10762        let program = Program::parse(initial_source).unwrap().0.unwrap();
10763        let mut frontend = FrontendState::new();
10764
10765        let mock_ctx = ExecutorContext::new_mock(None).await;
10766        let version = Version(0);
10767
10768        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10769        frontend.program = program;
10770        frontend.update_state_after_exec(outcome, true);
10771        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10772        let sketch_id = sketch_object.id;
10773        let sketch = expect_sketch(sketch_object);
10774
10775        let circle_id = sketch
10776            .segments
10777            .iter()
10778            .copied()
10779            .find(|seg_id| {
10780                matches!(
10781                    &frontend.scene_graph.objects[seg_id.0].kind,
10782                    ObjectKind::Segment {
10783                        segment: Segment::Circle(_)
10784                    }
10785                )
10786            })
10787            .expect("Expected a circle segment in sketch");
10788        let line_id = frontend
10789            .scene_graph
10790            .objects
10791            .iter()
10792            .find_map(|obj| match &obj.kind {
10793                ObjectKind::Segment {
10794                    segment: Segment::Line(line),
10795                } if line.owner.is_none() => Some(obj.id),
10796                _ => None,
10797            })
10798            .expect("Expected a standalone line segment in scene graph");
10799
10800        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10801            ObjectKind::Segment {
10802                segment: Segment::Line(line),
10803            } => line.start,
10804            _ => panic!("Expected line segment object"),
10805        };
10806
10807        let constraint = Constraint::Coincident(Coincident {
10808            segments: vec![line_start_point_id.into(), circle_id.into()],
10809        });
10810        let (src_delta, _scene_delta) = frontend
10811            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10812            .await
10813            .unwrap();
10814        insta::assert_snapshot!(
10815            "test_coincident_of_line_point_and_circle_segment",
10816            src_delta.text.as_str()
10817        );
10818
10819        mock_ctx.close().await;
10820    }
10821
10822    #[tokio::test(flavor = "multi_thread")]
10823    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10824        // Test that attempting an invalid coincident constraint (arc and line)
10825        // doesn't corrupt the state, allowing subsequent operations to work.
10826        // This test verifies the transactional fix in add_constraint that prevents
10827        // state corruption when invalid constraints are attempted.
10828        // Example: coincident constraint between an arc segment and a straight line segment
10829        // is geometrically invalid and should fail, but state should remain intact.
10830        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10831        let program = Program::empty();
10832
10833        let mut frontend = FrontendState::new();
10834        frontend.program = program;
10835
10836        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10837        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10838        let version = Version(0);
10839
10840        let sketch_args = SketchCtor {
10841            on: Plane::Default(PlaneName::Xy),
10842        };
10843        let (_src_delta, _scene_delta, sketch_id) = frontend
10844            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10845            .await
10846            .unwrap();
10847
10848        // Add an arc segment
10849        let arc_ctor = ArcCtor {
10850            start: Point2d {
10851                x: Expr::Var(Number {
10852                    value: 0.0,
10853                    units: NumericSuffix::Mm,
10854                }),
10855                y: Expr::Var(Number {
10856                    value: 0.0,
10857                    units: NumericSuffix::Mm,
10858                }),
10859            },
10860            end: Point2d {
10861                x: Expr::Var(Number {
10862                    value: 10.0,
10863                    units: NumericSuffix::Mm,
10864                }),
10865                y: Expr::Var(Number {
10866                    value: 10.0,
10867                    units: NumericSuffix::Mm,
10868                }),
10869            },
10870            center: Point2d {
10871                x: Expr::Var(Number {
10872                    value: 10.0,
10873                    units: NumericSuffix::Mm,
10874                }),
10875                y: Expr::Var(Number {
10876                    value: 0.0,
10877                    units: NumericSuffix::Mm,
10878                }),
10879            },
10880            direction: None,
10881            construction: None,
10882        };
10883        let (_src_delta, scene_delta) = frontend
10884            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10885            .await
10886            .unwrap();
10887        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10888        let arc_id = *scene_delta.new_objects.last().unwrap();
10889
10890        // Add a line segment
10891        let line_ctor = LineCtor {
10892            start: Point2d {
10893                x: Expr::Var(Number {
10894                    value: 20.0,
10895                    units: NumericSuffix::Mm,
10896                }),
10897                y: Expr::Var(Number {
10898                    value: 0.0,
10899                    units: NumericSuffix::Mm,
10900                }),
10901            },
10902            end: Point2d {
10903                x: Expr::Var(Number {
10904                    value: 30.0,
10905                    units: NumericSuffix::Mm,
10906                }),
10907                y: Expr::Var(Number {
10908                    value: 10.0,
10909                    units: NumericSuffix::Mm,
10910                }),
10911            },
10912            construction: None,
10913        };
10914        let (_src_delta, scene_delta) = frontend
10915            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10916            .await
10917            .unwrap();
10918        // The line is the last object in new_objects (after the 2 points: start, end)
10919        let line_id = *scene_delta.new_objects.last().unwrap();
10920
10921        // Attempt to add an invalid coincident constraint between arc and line
10922        // This should fail during execution, but state should remain intact
10923        let constraint = Constraint::Coincident(Coincident {
10924            segments: vec![arc_id.into(), line_id.into()],
10925        });
10926        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10927
10928        // The constraint addition should fail (invalid constraint)
10929        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10930
10931        // Verify state is not corrupted by checking that we can still access the scene graph
10932        // and that the original segments are still present with their source ranges
10933        let sketch_object_after =
10934            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10935        let sketch_after = expect_sketch(sketch_object_after);
10936
10937        // Verify both segments are still in the sketch
10938        assert!(
10939            sketch_after.segments.contains(&arc_id),
10940            "Arc segment should still exist after failed constraint"
10941        );
10942        assert!(
10943            sketch_after.segments.contains(&line_id),
10944            "Line segment should still exist after failed constraint"
10945        );
10946
10947        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10948        let arc_obj = frontend
10949            .scene_graph
10950            .objects
10951            .get(arc_id.0)
10952            .expect("Arc object should still be accessible");
10953        let line_obj = frontend
10954            .scene_graph
10955            .objects
10956            .get(line_id.0)
10957            .expect("Line object should still be accessible");
10958
10959        // Verify source ranges are still valid (not corrupted)
10960        // Just verify that the objects are still accessible and have the expected types
10961        match &arc_obj.kind {
10962            ObjectKind::Segment {
10963                segment: Segment::Arc(_),
10964            } => {}
10965            _ => panic!("Arc object should still be an arc segment"),
10966        }
10967        match &line_obj.kind {
10968            ObjectKind::Segment {
10969                segment: Segment::Line(_),
10970            } => {}
10971            _ => panic!("Line object should still be a line segment"),
10972        }
10973
10974        ctx.close().await;
10975        mock_ctx.close().await;
10976    }
10977
10978    #[tokio::test(flavor = "multi_thread")]
10979    async fn test_distance_two_points() {
10980        let initial_source = "\
10981sketch(on = XY) {
10982  point(at = [var 1, var 2])
10983  point(at = [var 3, var 4])
10984}
10985";
10986
10987        let program = Program::parse(initial_source).unwrap().0.unwrap();
10988
10989        let mut frontend = FrontendState::new();
10990
10991        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10992        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
10993        let version = Version(0);
10994
10995        frontend.hack_set_program(&ctx, program).await.unwrap();
10996        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10997        let sketch_id = sketch_object.id;
10998        let sketch = expect_sketch(sketch_object);
10999        let point0_id = *sketch.segments.first().unwrap();
11000        let point1_id = *sketch.segments.get(1).unwrap();
11001
11002        let constraint = Constraint::Distance(Distance {
11003            segments: vec![point0_id.into(), point1_id.into()],
11004            distance: Number {
11005                value: 2.0,
11006                units: NumericSuffix::Mm,
11007            },
11008            label_position: None,
11009            source: Default::default(),
11010        });
11011        let (src_delta, scene_delta) = frontend
11012            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11013            .await
11014            .unwrap();
11015        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11016        assert_eq!(
11017            scene_delta.new_graph.objects.len(),
11018            5,
11019            "{:#?}",
11020            scene_delta.new_graph.objects
11021        );
11022
11023        ctx.close().await;
11024        mock_ctx.close().await;
11025    }
11026
11027    #[tokio::test(flavor = "multi_thread")]
11028    async fn test_distance_two_points_with_label() {
11029        let initial_source = "\
11030sketch(on = XY) {
11031  point(at = [var 1, var 2])
11032  point(at = [var 3, var 4])
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 label_position = Point2d {
11053            x: Number {
11054                value: 10.0,
11055                units: NumericSuffix::Mm,
11056            },
11057            y: Number {
11058                value: 11.0,
11059                units: NumericSuffix::Mm,
11060            },
11061        };
11062        let constraint = Constraint::Distance(Distance {
11063            segments: vec![point0_id.into(), point1_id.into()],
11064            distance: Number {
11065                value: 2.0,
11066                units: NumericSuffix::Mm,
11067            },
11068            label_position: Some(label_position.clone()),
11069            source: Default::default(),
11070        });
11071        let (src_delta, scene_delta) = frontend
11072            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11073            .await
11074            .unwrap();
11075        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11076
11077        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11078        let sketch = expect_sketch(sketch_object);
11079        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11080        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11081            panic!("Expected constraint object");
11082        };
11083        let Constraint::Distance(distance) = constraint else {
11084            panic!("Expected distance constraint");
11085        };
11086        assert_eq!(distance.label_position, Some(label_position));
11087
11088        mock_ctx.close().await;
11089    }
11090
11091    #[tokio::test(flavor = "multi_thread")]
11092    async fn test_edit_distance_constraint_label_position() {
11093        let initial_source = "\
11094sketch(on = XY) {
11095  point(at = [var 1, var 2])
11096  point(at = [var 3, var 2])
11097}
11098";
11099
11100        let program = Program::parse(initial_source).unwrap().0.unwrap();
11101
11102        let mut frontend = FrontendState::new();
11103
11104        let mock_ctx = ExecutorContext::new_mock(None).await;
11105        let version = Version(0);
11106
11107        frontend.program = program.clone();
11108        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11109        frontend.update_state_after_exec(outcome, true);
11110        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11111        let sketch_id = sketch_object.id;
11112        let sketch = expect_sketch(sketch_object);
11113        let point0_id = *sketch.segments.first().unwrap();
11114        let point1_id = *sketch.segments.get(1).unwrap();
11115
11116        let constraint = Constraint::Distance(Distance {
11117            segments: vec![point0_id.into(), point1_id.into()],
11118            distance: Number {
11119                value: 2.0,
11120                units: NumericSuffix::Mm,
11121            },
11122            label_position: None,
11123            source: Default::default(),
11124        });
11125        let (_, scene_delta) = frontend
11126            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11127            .await
11128            .unwrap();
11129        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11130        let sketch = expect_sketch(sketch_object);
11131        let constraint_id = sketch.constraints[0];
11132        let label_position = Point2d {
11133            x: Number {
11134                value: 10.0,
11135                units: NumericSuffix::Mm,
11136            },
11137            y: Number {
11138                value: 11.0,
11139                units: NumericSuffix::Mm,
11140            },
11141        };
11142
11143        let (src_delta, scene_delta) = frontend
11144            .edit_distance_constraint_label_position(
11145                &mock_ctx,
11146                version,
11147                sketch_id,
11148                constraint_id,
11149                label_position.clone(),
11150                vec![],
11151            )
11152            .await
11153            .unwrap();
11154        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11155
11156        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11157        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11158            panic!("Expected constraint object");
11159        };
11160        let Constraint::Distance(distance) = constraint else {
11161            panic!("Expected distance constraint");
11162        };
11163        assert_eq!(distance.label_position, Some(label_position));
11164
11165        mock_ctx.close().await;
11166    }
11167
11168    #[tokio::test(flavor = "multi_thread")]
11169    async fn test_edit_distance_constraint_type_and_value() {
11170        let initial_source = "\
11171sketch(on = XY) {
11172  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11173  distance([line1.start, line1.end]) == 5mm
11174}
11175";
11176
11177        let program = Program::parse(initial_source).unwrap().0.unwrap();
11178        let mut frontend = FrontendState::new();
11179        let mock_ctx = ExecutorContext::new_mock(None).await;
11180        let version = Version(0);
11181
11182        frontend.program = program.clone();
11183        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11184        frontend.update_state_after_exec(outcome, true);
11185        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11186        let sketch_id = sketch_object.id;
11187        let sketch = expect_sketch(sketch_object);
11188        let constraint_id = sketch.constraints[0];
11189        let point0_id = sketch.segments[0];
11190        let point1_id = sketch.segments[1];
11191        let label_position = Point2d {
11192            x: Number {
11193                value: 2.0,
11194                units: NumericSuffix::Mm,
11195            },
11196            y: Number {
11197                value: 5.0,
11198                units: NumericSuffix::Mm,
11199            },
11200        };
11201
11202        let (source_delta, scene_delta) = frontend
11203            .edit_distance_constraint_with_options(
11204                &mock_ctx,
11205                version,
11206                sketch_id,
11207                constraint_id,
11208                Constraint::HorizontalDistance(Distance {
11209                    segments: vec![point0_id.into(), point1_id.into()],
11210                    distance: Number {
11211                        value: 4.0,
11212                        units: NumericSuffix::Mm,
11213                    },
11214                    label_position: Some(label_position.clone()),
11215                    source: Default::default(),
11216                }),
11217                EditConstraintOptions {
11218                    commit_solved_initial_guesses: false,
11219                },
11220            )
11221            .await
11222            .unwrap();
11223        assert_eq!(
11224            source_delta.text,
11225            "\
11226sketch(on = XY) {
11227  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11228  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11229}
11230"
11231        );
11232
11233        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11234        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11235            panic!("Expected constraint object");
11236        };
11237        let Constraint::HorizontalDistance(distance) = constraint else {
11238            panic!("Expected horizontal distance constraint");
11239        };
11240        assert_eq!(distance.distance.value, 4.0);
11241        assert_eq!(distance.label_position, Some(label_position));
11242
11243        mock_ctx.close().await;
11244    }
11245
11246    #[tokio::test(flavor = "multi_thread")]
11247    async fn test_edit_angle_constraint_label_position() {
11248        let initial_source = "\
11249sketch(on = XY) {
11250  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11251  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11252  angle([line1, line2]) == 60deg
11253}
11254";
11255
11256        let program = Program::parse(initial_source).unwrap().0.unwrap();
11257        let mut frontend = FrontendState::new();
11258        let mock_ctx = ExecutorContext::new_mock(None).await;
11259        let version = Version(0);
11260
11261        frontend.program = program.clone();
11262        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11263        frontend.update_state_after_exec(outcome, true);
11264        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11265        let sketch_id = sketch_object.id;
11266        let sketch = expect_sketch(sketch_object);
11267        let constraint_id = sketch.constraints[0];
11268        let label_position = Point2d {
11269            x: Number {
11270                value: 10.0,
11271                units: NumericSuffix::Mm,
11272            },
11273            y: Number {
11274                value: 11.0,
11275                units: NumericSuffix::Mm,
11276            },
11277        };
11278
11279        let (src_delta, scene_delta) = frontend
11280            .edit_distance_constraint_label_position(
11281                &mock_ctx,
11282                version,
11283                sketch_id,
11284                constraint_id,
11285                label_position.clone(),
11286                vec![],
11287            )
11288            .await
11289            .unwrap();
11290        assert_eq!(
11291            src_delta.text.as_str(),
11292            "\
11293sketch(on = XY) {
11294  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11295  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11296  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11297}
11298"
11299        );
11300
11301        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11302        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11303            panic!("Expected constraint object");
11304        };
11305        let Constraint::Angle(angle) = constraint else {
11306            panic!("Expected angle constraint");
11307        };
11308        assert_eq!(angle.label_position, Some(label_position));
11309
11310        mock_ctx.close().await;
11311    }
11312
11313    #[tokio::test(flavor = "multi_thread")]
11314    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11315        let initial_source = "\
11316sketch(on = XY) {
11317  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11318  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11319  60deg == angleDimension(lines = [line1, line2], sector = 1)
11320}
11321";
11322
11323        let program = Program::parse(initial_source).unwrap().0.unwrap();
11324        let mut frontend = FrontendState::new();
11325        let mock_ctx = ExecutorContext::new_mock(None).await;
11326        let version = Version(0);
11327
11328        frontend.program = program.clone();
11329        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11330        frontend.update_state_after_exec(outcome, true);
11331        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11332        let sketch_id = sketch_object.id;
11333        let sketch = expect_sketch(sketch_object);
11334        let constraint_id = sketch.constraints[0];
11335        let label_position = Point2d {
11336            x: Number {
11337                value: 10.0,
11338                units: NumericSuffix::Mm,
11339            },
11340            y: Number {
11341                value: 11.0,
11342                units: NumericSuffix::Mm,
11343            },
11344        };
11345
11346        let (src_delta, scene_delta) = frontend
11347            .edit_distance_constraint_label_position(
11348                &mock_ctx,
11349                version,
11350                sketch_id,
11351                constraint_id,
11352                label_position.clone(),
11353                vec![],
11354            )
11355            .await
11356            .unwrap();
11357        assert_eq!(
11358            src_delta.text.as_str(),
11359            "\
11360sketch(on = XY) {
11361  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11362  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11363  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11364}
11365"
11366        );
11367
11368        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11369        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11370            panic!("Expected constraint object");
11371        };
11372        let Constraint::Angle(angle) = constraint else {
11373            panic!("Expected angle constraint");
11374        };
11375        assert_eq!(angle.label_position, Some(label_position));
11376
11377        mock_ctx.close().await;
11378    }
11379
11380    #[tokio::test(flavor = "multi_thread")]
11381    async fn test_edit_angle_constraint() {
11382        let initial_source = "\
11383sketch(on = XY) {
11384  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11385  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11386  angle([line1, line2]) == 60deg
11387}
11388";
11389
11390        let program = Program::parse(initial_source).unwrap().0.unwrap();
11391        let mut frontend = FrontendState::new();
11392        let mock_ctx = ExecutorContext::new_mock(None).await;
11393        let version = Version(0);
11394
11395        frontend.program = program.clone();
11396        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11397        frontend.update_state_after_exec(outcome, true);
11398        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11399        let sketch_id = sketch_object.id;
11400        let sketch = expect_sketch(sketch_object);
11401        let constraint_id = sketch.constraints[0];
11402        let line1_id = *sketch.segments.get(2).unwrap();
11403        let line2_id = *sketch.segments.get(5).unwrap();
11404        let label_position = Point2d {
11405            x: Number {
11406                value: 10.0,
11407                units: NumericSuffix::Mm,
11408            },
11409            y: Number {
11410                value: 11.0,
11411                units: NumericSuffix::Mm,
11412            },
11413        };
11414
11415        let (src_delta, scene_delta) = frontend
11416            .edit_angle_constraint_with_options(
11417                &mock_ctx,
11418                version,
11419                sketch_id,
11420                constraint_id,
11421                Angle {
11422                    lines: vec![line2_id, line1_id],
11423                    angle: Number {
11424                        value: 60.0,
11425                        units: NumericSuffix::Deg,
11426                    },
11427                    sector: Some(3),
11428                    inverse: Some(false),
11429                    label_position: Some(label_position.clone()),
11430                    source: Default::default(),
11431                },
11432                EditConstraintOptions {
11433                    commit_solved_initial_guesses: false,
11434                },
11435            )
11436            .await
11437            .unwrap();
11438        assert_eq!(
11439            src_delta.text.as_str(),
11440            "\
11441sketch(on = XY) {
11442  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11443  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11444  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11445}
11446"
11447        );
11448
11449        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11450        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11451            panic!("Expected constraint object");
11452        };
11453        let Constraint::Angle(angle) = constraint else {
11454            panic!("Expected angle constraint");
11455        };
11456        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11457        assert_eq!(angle.sector, Some(3));
11458        assert_eq!(angle.inverse, Some(false));
11459        assert_eq!(angle.label_position, Some(label_position));
11460
11461        mock_ctx.close().await;
11462    }
11463
11464    #[tokio::test(flavor = "multi_thread")]
11465    async fn test_edit_angle_constraint_with_call_on_right() {
11466        let initial_source = "\
11467sketch(on = XY) {
11468  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11469  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11470  60deg == angle([line1, line2])
11471}
11472";
11473
11474        let program = Program::parse(initial_source).unwrap().0.unwrap();
11475        let mut frontend = FrontendState::new();
11476        let mock_ctx = ExecutorContext::new_mock(None).await;
11477        let version = Version(0);
11478
11479        frontend.program = program.clone();
11480        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11481        frontend.update_state_after_exec(outcome, true);
11482        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11483        let sketch_id = sketch_object.id;
11484        let sketch = expect_sketch(sketch_object);
11485        let constraint_id = sketch.constraints[0];
11486        let line1_id = *sketch.segments.get(2).unwrap();
11487        let line2_id = *sketch.segments.get(5).unwrap();
11488
11489        let (src_delta, _) = frontend
11490            .edit_angle_constraint_with_options(
11491                &mock_ctx,
11492                version,
11493                sketch_id,
11494                constraint_id,
11495                Angle {
11496                    lines: vec![line2_id, line1_id],
11497                    angle: Number {
11498                        value: 60.0,
11499                        units: NumericSuffix::Deg,
11500                    },
11501                    sector: Some(3),
11502                    inverse: Some(false),
11503                    label_position: None,
11504                    source: Default::default(),
11505                },
11506                EditConstraintOptions {
11507                    commit_solved_initial_guesses: false,
11508                },
11509            )
11510            .await
11511            .unwrap();
11512        assert_eq!(
11513            src_delta.text.as_str(),
11514            "\
11515sketch(on = XY) {
11516  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11517  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11518  60deg == angleDimension(lines = [line2, line1], sector = 3)
11519}
11520"
11521        );
11522
11523        mock_ctx.close().await;
11524    }
11525
11526    #[tokio::test(flavor = "multi_thread")]
11527    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11528        let initial_source = "\
11529@settings(kclVersion = 2.0)
11530
11531sketch001 = sketch(on = XZ) {
11532  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11533  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11534  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11535  vertical([line1.start, ORIGIN])
11536}
11537";
11538
11539        let program = Program::parse(initial_source).unwrap().0.unwrap();
11540        let mut frontend = FrontendState::new();
11541        let mock_ctx = ExecutorContext::new_mock(None).await;
11542        let version = Version(0);
11543
11544        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11545        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11546        let sketch_id = sketch_object.id;
11547        let sketch = expect_sketch(sketch_object);
11548        let constraint_id = sketch
11549            .constraints
11550            .iter()
11551            .copied()
11552            .find(|constraint_id| {
11553                matches!(
11554                    frontend.scene_graph.objects[constraint_id.0].kind,
11555                    ObjectKind::Constraint {
11556                        constraint: Constraint::Distance(_)
11557                    }
11558                )
11559            })
11560            .unwrap();
11561        let line1_id = sketch
11562            .segments
11563            .iter()
11564            .copied()
11565            .find(|segment_id| {
11566                matches!(
11567                    frontend.scene_graph.objects[segment_id.0].kind,
11568                    ObjectKind::Segment {
11569                        segment: Segment::Line(_)
11570                    }
11571                )
11572            })
11573            .unwrap();
11574        let label_position = Point2d {
11575            x: Number {
11576                value: 7.0,
11577                units: NumericSuffix::Mm,
11578            },
11579            y: Number {
11580                value: 9.0,
11581                units: NumericSuffix::Mm,
11582            },
11583        };
11584
11585        let (source_delta, scene_delta) = frontend
11586            .edit_segments_with_options(
11587                &mock_ctx,
11588                version,
11589                sketch_id,
11590                vec![ExistingSegmentCtor {
11591                    id: line1_id,
11592                    ctor: SegmentCtor::Line(LineCtor {
11593                        start: point_expr_mm(2.0, 15.55),
11594                        end: point_expr_mm(-4.03, 11.51),
11595                        construction: None,
11596                    }),
11597                }],
11598                EditSegmentsOptions {
11599                    anchor_segment_ids: Some(vec![]),
11600                    drag_anchors: vec![SegmentDragAnchor {
11601                        segment_id: line1_id,
11602                        target: label_position.clone(),
11603                    }],
11604                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11605                        constraint_id,
11606                        label_position: label_position.clone(),
11607                    }],
11608                    commit_solved_initial_guesses: true,
11609                },
11610            )
11611            .await
11612            .unwrap();
11613
11614        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11615        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11616        let ObjectKind::Constraint {
11617            constraint: Constraint::Distance(distance),
11618        } = &constraint_object.kind
11619        else {
11620            panic!("Expected distance constraint object");
11621        };
11622        assert_eq!(distance.label_position, Some(label_position));
11623
11624        let snapped_label_position = Point2d {
11625            x: Number {
11626                value: 8.0,
11627                units: NumericSuffix::Mm,
11628            },
11629            y: Number {
11630                value: 10.0,
11631                units: NumericSuffix::Mm,
11632            },
11633        };
11634        let (source_delta, scene_delta) = frontend
11635            .edit_segments_with_options(
11636                &mock_ctx,
11637                version,
11638                sketch_id,
11639                vec![],
11640                EditSegmentsOptions {
11641                    anchor_segment_ids: Some(vec![line1_id]),
11642                    drag_anchors: vec![],
11643                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11644                        constraint_id,
11645                        label_position: snapped_label_position.clone(),
11646                    }],
11647                    commit_solved_initial_guesses: true,
11648                },
11649            )
11650            .await
11651            .unwrap();
11652
11653        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11654        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11655        let ObjectKind::Constraint {
11656            constraint: Constraint::Distance(distance),
11657        } = &constraint_object.kind
11658        else {
11659            panic!("Expected distance constraint object");
11660        };
11661        assert_eq!(distance.label_position, Some(snapped_label_position));
11662
11663        mock_ctx.close().await;
11664    }
11665
11666    #[tokio::test(flavor = "multi_thread")]
11667    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11668        let initial_source = "\
11669sketch(on = XY) {
11670  point1 = point(at = [var 0mm, var 0mm])
11671  point2 = point(at = [var 10mm, var 0mm])
11672  distance([point1, point2]) == 5mm
11673}
11674";
11675
11676        let program = Program::parse(initial_source).unwrap().0.unwrap();
11677        let mut frontend = FrontendState::new();
11678        let mock_ctx = ExecutorContext::new_mock(None).await;
11679        let version = Version(0);
11680
11681        frontend.program = program.clone();
11682        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11683        frontend.update_state_after_exec(outcome, true);
11684        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11685        let sketch_id = sketch_object.id;
11686        let sketch = expect_sketch(sketch_object);
11687        let point0_id = sketch.segments[0];
11688        let point1_id = sketch.segments[1];
11689        let constraint_id = sketch.constraints[0];
11690
11691        let edited_segments = vec![ExistingSegmentCtor {
11692            id: point0_id,
11693            ctor: SegmentCtor::Point(PointCtor {
11694                position: Point2d {
11695                    x: Expr::Var(Number {
11696                        value: 2.0,
11697                        units: NumericSuffix::Mm,
11698                    }),
11699                    y: Expr::Var(Number {
11700                        value: 1.0,
11701                        units: NumericSuffix::Mm,
11702                    }),
11703                },
11704            }),
11705        }];
11706        let (_, scene_delta) = frontend
11707            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11708            .await
11709            .unwrap();
11710        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11711        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11712
11713        let label_position = Point2d {
11714            x: Number {
11715                value: 3.0,
11716                units: NumericSuffix::Mm,
11717            },
11718            y: Number {
11719                value: 4.0,
11720                units: NumericSuffix::Mm,
11721            },
11722        };
11723        let (_, scene_delta) = frontend
11724            .edit_distance_constraint_label_position(
11725                &mock_ctx,
11726                version,
11727                sketch_id,
11728                constraint_id,
11729                label_position,
11730                vec![point0_id],
11731            )
11732            .await
11733            .unwrap();
11734
11735        assert_point_position_close(
11736            point_position(&scene_delta.new_graph, point0_id),
11737            point0_after_segment_edit,
11738        );
11739        assert_point_position_close(
11740            point_position(&scene_delta.new_graph, point1_id),
11741            point1_after_segment_edit,
11742        );
11743
11744        mock_ctx.close().await;
11745    }
11746
11747    #[tokio::test(flavor = "multi_thread")]
11748    async fn test_distance_point_line() {
11749        let initial_source = "\
11750sketch(on = XY) {
11751  point(at = [var 0, var 5])
11752  line(start = [var 0, var 0], end = [var 10, var 0])
11753}
11754";
11755
11756        let program = Program::parse(initial_source).unwrap().0.unwrap();
11757
11758        let mut frontend = FrontendState::new();
11759
11760        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11761        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11762        let version = Version(0);
11763
11764        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11765        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11766        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11767        let sketch_id = sketch_object.id;
11768        let sketch = expect_sketch(sketch_object);
11769        let point_id = *sketch.segments.first().unwrap();
11770        let line_id = *sketch
11771            .segments
11772            .iter()
11773            .find(|segment_id| {
11774                matches!(
11775                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11776                    Some(ObjectKind::Segment {
11777                        segment: Segment::Line(_)
11778                    })
11779                )
11780            })
11781            .unwrap();
11782
11783        let label_position = Point2d {
11784            x: Number {
11785                value: 10.0,
11786                units: NumericSuffix::Mm,
11787            },
11788            y: Number {
11789                value: 11.0,
11790                units: NumericSuffix::Mm,
11791            },
11792        };
11793        let constraint = Constraint::Distance(Distance {
11794            segments: vec![point_id.into(), line_id.into()],
11795            distance: Number {
11796                value: 5.0,
11797                units: NumericSuffix::Mm,
11798            },
11799            label_position: Some(label_position.clone()),
11800            source: Default::default(),
11801        });
11802        let (src_delta, scene_delta) = frontend
11803            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11804            .await
11805            .unwrap();
11806        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11807        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11808        let sketch = expect_sketch(sketch_object);
11809        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11810        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11811            panic!("Expected constraint object");
11812        };
11813        let Constraint::Distance(distance) = constraint else {
11814            panic!("Expected distance constraint");
11815        };
11816        assert_eq!(distance.label_position, Some(label_position));
11817
11818        ctx.close().await;
11819        mock_ctx.close().await;
11820    }
11821
11822    #[tokio::test(flavor = "multi_thread")]
11823    async fn test_distance_point_arc() {
11824        let initial_source = "\
11825sketch(on = XY) {
11826  point(at = [var 0, var 8])
11827  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11828}
11829";
11830
11831        let program = Program::parse(initial_source).unwrap().0.unwrap();
11832
11833        let mut frontend = FrontendState::new();
11834
11835        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11836        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
11837        let version = Version(0);
11838
11839        frontend.hack_set_program(&ctx, program).await.unwrap();
11840        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11841        let sketch_id = sketch_object.id;
11842        let sketch = expect_sketch(sketch_object);
11843        let point_id = *sketch.segments.first().unwrap();
11844        let arc_id = *sketch
11845            .segments
11846            .iter()
11847            .find(|segment_id| {
11848                matches!(
11849                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11850                    Some(ObjectKind::Segment {
11851                        segment: Segment::Arc(_)
11852                    })
11853                )
11854            })
11855            .unwrap();
11856
11857        let constraint = Constraint::Distance(Distance {
11858            segments: vec![point_id.into(), arc_id.into()],
11859            distance: Number {
11860                value: 3.0,
11861                units: NumericSuffix::Mm,
11862            },
11863            label_position: None,
11864            source: Default::default(),
11865        });
11866        let (src_delta, _scene_delta) = frontend
11867            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11868            .await
11869            .unwrap();
11870        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11871
11872        ctx.close().await;
11873        mock_ctx.close().await;
11874    }
11875
11876    #[tokio::test(flavor = "multi_thread")]
11877    async fn test_distance_arc_origin() {
11878        let initial_source = "\
11879sketch001 = sketch(on = XY) {
11880  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11881}
11882";
11883
11884        let program = Program::parse(initial_source).unwrap().0.unwrap();
11885
11886        let mut frontend = FrontendState::new();
11887
11888        let mock_ctx = ExecutorContext::new_mock(None).await;
11889        let version = Version(0);
11890
11891        frontend.program = program.clone();
11892        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11893        frontend.update_state_after_exec(outcome, true);
11894        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11895        let sketch_id = sketch_object.id;
11896        let sketch = expect_sketch(sketch_object);
11897        let arc_id = *sketch
11898            .segments
11899            .iter()
11900            .find(|segment_id| {
11901                matches!(
11902                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11903                    Some(ObjectKind::Segment {
11904                        segment: Segment::Arc(_)
11905                    })
11906                )
11907            })
11908            .unwrap();
11909
11910        let constraint = Constraint::Distance(Distance {
11911            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11912            distance: Number {
11913                value: 3.0,
11914                units: NumericSuffix::Mm,
11915            },
11916            label_position: None,
11917            source: Default::default(),
11918        });
11919        let (src_delta, _scene_delta) = frontend
11920            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11921            .await
11922            .unwrap();
11923        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11924
11925        mock_ctx.close().await;
11926    }
11927
11928    #[tokio::test(flavor = "multi_thread")]
11929    async fn test_distance_line_origin() {
11930        let initial_source = "\
11931sketch(on = XY) {
11932  line(start = [var 5, var 0], end = [var 5, var 10])
11933}
11934";
11935
11936        let program = Program::parse(initial_source).unwrap().0.unwrap();
11937
11938        let mut frontend = FrontendState::new();
11939
11940        let mock_ctx = ExecutorContext::new_mock(None).await;
11941        let version = Version(0);
11942
11943        frontend.program = program.clone();
11944        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11945        frontend.update_state_after_exec(outcome, true);
11946        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11947        let sketch_id = sketch_object.id;
11948        let sketch = expect_sketch(sketch_object);
11949        let line_id = *sketch
11950            .segments
11951            .iter()
11952            .find(|segment_id| {
11953                matches!(
11954                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11955                    Some(ObjectKind::Segment {
11956                        segment: Segment::Line(_)
11957                    })
11958                )
11959            })
11960            .unwrap();
11961
11962        let constraint = Constraint::Distance(Distance {
11963            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11964            distance: Number {
11965                value: 5.0,
11966                units: NumericSuffix::Mm,
11967            },
11968            label_position: None,
11969            source: Default::default(),
11970        });
11971        let (src_delta, _scene_delta) = frontend
11972            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11973            .await
11974            .unwrap();
11975        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11976
11977        mock_ctx.close().await;
11978    }
11979
11980    #[tokio::test(flavor = "multi_thread")]
11981    async fn test_distance_line_circle() {
11982        let initial_source = "\
11983sketch(on = XY) {
11984  line(start = [var -10, var 8], end = [var 10, var 8])
11985  circle(start = [var 5, var 0], center = [var 0, 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_geometry_only_with_default_client().await.unwrap();
11994        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
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 line_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::Line(_)
12009                    })
12010                )
12011            })
12012            .unwrap();
12013        let circle_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::Circle(_)
12021                    })
12022                )
12023            })
12024            .unwrap();
12025
12026        let constraint = Constraint::Distance(Distance {
12027            segments: vec![line_id.into(), circle_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_line_circle", 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_circle_arc() {
12047        let initial_source = "\
12048sketch(on = XY) {
12049  circle(start = [var 5, var 0], center = [var 0, var 0])
12050  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
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_geometry_only_with_default_client().await.unwrap();
12059        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12060        let version = Version(0);
12061
12062        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12063        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12064        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12065        let sketch_id = sketch_object.id;
12066        let sketch = expect_sketch(sketch_object);
12067        let circle_id = *sketch
12068            .segments
12069            .iter()
12070            .find(|segment_id| {
12071                matches!(
12072                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12073                    Some(ObjectKind::Segment {
12074                        segment: Segment::Circle(_)
12075                    })
12076                )
12077            })
12078            .unwrap();
12079        let arc_id = *sketch
12080            .segments
12081            .iter()
12082            .find(|segment_id| {
12083                matches!(
12084                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12085                    Some(ObjectKind::Segment {
12086                        segment: Segment::Arc(_)
12087                    })
12088                )
12089            })
12090            .unwrap();
12091
12092        let constraint = Constraint::Distance(Distance {
12093            segments: vec![circle_id.into(), arc_id.into()],
12094            distance: Number {
12095                value: 3.0,
12096                units: NumericSuffix::Mm,
12097            },
12098            label_position: None,
12099            source: Default::default(),
12100        });
12101        let (src_delta, _scene_delta) = frontend
12102            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12103            .await
12104            .unwrap();
12105        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12106
12107        ctx.close().await;
12108        mock_ctx.close().await;
12109    }
12110
12111    #[tokio::test(flavor = "multi_thread")]
12112    async fn test_distance_parallel_lines() {
12113        let initial_source = "\
12114sketch(on = XY) {
12115  line(start = [var 0, var 0], end = [var 10, var 0])
12116  line(start = [var 0, var 5], end = [var 10, var 5])
12117}
12118";
12119
12120        let program = Program::parse(initial_source).unwrap().0.unwrap();
12121
12122        let mut frontend = FrontendState::new();
12123
12124        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12125        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12126        let version = Version(0);
12127
12128        frontend.hack_set_program(&ctx, program).await.unwrap();
12129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12130        let sketch_id = sketch_object.id;
12131        let sketch = expect_sketch(sketch_object);
12132        let line_ids = sketch
12133            .segments
12134            .iter()
12135            .copied()
12136            .filter(|segment_id| {
12137                matches!(
12138                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12139                    Some(ObjectKind::Segment {
12140                        segment: Segment::Line(_)
12141                    })
12142                )
12143            })
12144            .collect::<Vec<_>>();
12145
12146        let constraint = Constraint::Distance(Distance {
12147            segments: vec![line_ids[0].into(), line_ids[1].into()],
12148            distance: Number {
12149                value: 5.0,
12150                units: NumericSuffix::Mm,
12151            },
12152            label_position: None,
12153            source: Default::default(),
12154        });
12155        let (src_delta, _scene_delta) = frontend
12156            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12157            .await
12158            .unwrap();
12159        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12160
12161        ctx.close().await;
12162        mock_ctx.close().await;
12163    }
12164
12165    #[tokio::test(flavor = "multi_thread")]
12166    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12167        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12168        // one another because the gradient of the residual has no tangential component in this
12169        // configuration. The only path to reducing the residual is shrinking the lengths of the
12170        // lines which are causing the lines to collapse, producing a degenerate output.
12171        let initial_source = "\
12172sketch(on = XY) {
12173  line(start = [var 0, var 0], end = [var 10, var 0])
12174  line(start = [var 0, var 0], end = [var 10, var 10])
12175}
12176";
12177
12178        let program = Program::parse(initial_source).unwrap().0.unwrap();
12179
12180        let mut frontend = FrontendState::new();
12181
12182        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12183        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12184        let version = Version(0);
12185
12186        frontend.hack_set_program(&ctx, program).await.unwrap();
12187        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12188        let sketch_id = sketch_object.id;
12189        let sketch = expect_sketch(sketch_object);
12190        let line_ids = sketch
12191            .segments
12192            .iter()
12193            .copied()
12194            .filter(|segment_id| {
12195                matches!(
12196                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12197                    Some(ObjectKind::Segment {
12198                        segment: Segment::Line(_)
12199                    })
12200                )
12201            })
12202            .collect::<Vec<_>>();
12203
12204        let constraint = Constraint::Distance(Distance {
12205            segments: vec![line_ids[0].into(), line_ids[1].into()],
12206            distance: Number {
12207                value: 5.0,
12208                units: NumericSuffix::Mm,
12209            },
12210            label_position: None,
12211            source: Default::default(),
12212        });
12213        let (src_delta, _scene_delta) = frontend
12214            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12215            .await
12216            .unwrap();
12217        insta::assert_snapshot!(
12218            "test_distance_non_parallel_lines_lowers_to_distance",
12219            src_delta.text.as_str()
12220        );
12221
12222        ctx.close().await;
12223        mock_ctx.close().await;
12224    }
12225
12226    #[tokio::test(flavor = "multi_thread")]
12227    async fn test_horizontal_distance_two_points() {
12228        let initial_source = "\
12229sketch(on = XY) {
12230  point(at = [var 1, var 2])
12231  point(at = [var 3, var 4])
12232}
12233";
12234
12235        let program = Program::parse(initial_source).unwrap().0.unwrap();
12236
12237        let mut frontend = FrontendState::new();
12238
12239        let mock_ctx = ExecutorContext::new_mock(None).await;
12240        let version = Version(0);
12241
12242        frontend.program = program.clone();
12243        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12244        frontend.update_state_after_exec(outcome, true);
12245        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12246        let sketch_id = sketch_object.id;
12247        let sketch = expect_sketch(sketch_object);
12248        let point0_id = *sketch.segments.first().unwrap();
12249        let point1_id = *sketch.segments.get(1).unwrap();
12250        let label_position = Point2d {
12251            x: Number {
12252                value: 10.0,
12253                units: NumericSuffix::Mm,
12254            },
12255            y: Number {
12256                value: 11.0,
12257                units: NumericSuffix::Mm,
12258            },
12259        };
12260
12261        let constraint = Constraint::HorizontalDistance(Distance {
12262            segments: vec![point0_id.into(), point1_id.into()],
12263            distance: Number {
12264                value: 2.0,
12265                units: NumericSuffix::Mm,
12266            },
12267            label_position: Some(label_position.clone()),
12268            source: Default::default(),
12269        });
12270        let (src_delta, scene_delta) = frontend
12271            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12272            .await
12273            .unwrap();
12274        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12275        assert_eq!(
12276            scene_delta.new_graph.objects.len(),
12277            5,
12278            "{:#?}",
12279            scene_delta.new_graph.objects
12280        );
12281        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12282        let sketch = expect_sketch(sketch_object);
12283        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12284        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12285            panic!("Expected constraint object");
12286        };
12287        let Constraint::HorizontalDistance(distance) = constraint else {
12288            panic!("Expected horizontal distance constraint");
12289        };
12290        assert_eq!(distance.label_position, Some(label_position));
12291
12292        mock_ctx.close().await;
12293    }
12294
12295    #[tokio::test(flavor = "multi_thread")]
12296    async fn test_radius_single_arc_segment() {
12297        let initial_source = "\
12298sketch(on = XY) {
12299  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12300}
12301";
12302
12303        let program = Program::parse(initial_source).unwrap().0.unwrap();
12304
12305        let mut frontend = FrontendState::new();
12306
12307        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12308        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12309        let version = Version(0);
12310
12311        frontend.hack_set_program(&ctx, program).await.unwrap();
12312        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12313        let sketch_id = sketch_object.id;
12314        let sketch = expect_sketch(sketch_object);
12315        // Find the arc segment (not the points)
12316        let arc_id = sketch
12317            .segments
12318            .iter()
12319            .find(|&seg_id| {
12320                let obj = frontend.scene_graph.objects.get(seg_id.0);
12321                matches!(
12322                    obj.map(|o| &o.kind),
12323                    Some(ObjectKind::Segment {
12324                        segment: Segment::Arc(_)
12325                    })
12326                )
12327            })
12328            .unwrap();
12329
12330        let constraint = Constraint::Radius(Radius {
12331            arc: *arc_id,
12332            radius: Number {
12333                value: 5.0,
12334                units: NumericSuffix::Mm,
12335            },
12336            label_position: None,
12337            source: Default::default(),
12338        });
12339        let (src_delta, scene_delta) = frontend
12340            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12341            .await
12342            .unwrap();
12343        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12344        assert_eq!(
12345            scene_delta.new_graph.objects.len(),
12346            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12347            "{:#?}",
12348            scene_delta.new_graph.objects
12349        );
12350
12351        ctx.close().await;
12352        mock_ctx.close().await;
12353    }
12354
12355    #[tokio::test(flavor = "multi_thread")]
12356    async fn test_radius_single_arc_segment_with_label_position() {
12357        let initial_source = "\
12358sketch(on = XY) {
12359  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12360}
12361";
12362
12363        let program = Program::parse(initial_source).unwrap().0.unwrap();
12364        let mut frontend = FrontendState::new();
12365        let mock_ctx = ExecutorContext::new_mock(None).await;
12366        let version = Version(0);
12367
12368        frontend.program = program.clone();
12369        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12370        frontend.update_state_after_exec(outcome, true);
12371        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12372        let sketch_id = sketch_object.id;
12373        let sketch = expect_sketch(sketch_object);
12374        let arc_id = sketch
12375            .segments
12376            .iter()
12377            .find(|&seg_id| {
12378                let obj = frontend.scene_graph.objects.get(seg_id.0);
12379                matches!(
12380                    obj.map(|o| &o.kind),
12381                    Some(ObjectKind::Segment {
12382                        segment: Segment::Arc(_)
12383                    })
12384                )
12385            })
12386            .unwrap();
12387
12388        let label_position = Point2d {
12389            x: Number {
12390                value: 10.0,
12391                units: NumericSuffix::Mm,
12392            },
12393            y: Number {
12394                value: 11.0,
12395                units: NumericSuffix::Mm,
12396            },
12397        };
12398        let constraint = Constraint::Radius(Radius {
12399            arc: *arc_id,
12400            radius: Number {
12401                value: 5.0,
12402                units: NumericSuffix::Mm,
12403            },
12404            label_position: Some(label_position.clone()),
12405            source: Default::default(),
12406        });
12407        let (src_delta, scene_delta) = frontend
12408            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12409            .await
12410            .unwrap();
12411        insta::assert_snapshot!(
12412            "test_radius_single_arc_segment_with_label_position",
12413            src_delta.text.as_str()
12414        );
12415
12416        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12417        let sketch = expect_sketch(sketch_object);
12418        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12419        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12420            panic!("Expected constraint object");
12421        };
12422        let Constraint::Radius(radius) = constraint else {
12423            panic!("Expected radius constraint");
12424        };
12425        assert_eq!(radius.label_position, Some(label_position));
12426
12427        mock_ctx.close().await;
12428    }
12429
12430    #[tokio::test(flavor = "multi_thread")]
12431    async fn test_edit_radius_constraint_label_position() {
12432        let initial_source = "\
12433sketch(on = XY) {
12434  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12435  radius(arc1) == 5mm
12436}
12437";
12438
12439        let program = Program::parse(initial_source).unwrap().0.unwrap();
12440        let mut frontend = FrontendState::new();
12441        let mock_ctx = ExecutorContext::new_mock(None).await;
12442        let version = Version(0);
12443
12444        frontend.program = program.clone();
12445        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12446        frontend.update_state_after_exec(outcome, true);
12447        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12448        let sketch_id = sketch_object.id;
12449        let sketch = expect_sketch(sketch_object);
12450        let constraint_id = sketch.constraints[0];
12451        let label_position = Point2d {
12452            x: Number {
12453                value: 10.0,
12454                units: NumericSuffix::Mm,
12455            },
12456            y: Number {
12457                value: 11.0,
12458                units: NumericSuffix::Mm,
12459            },
12460        };
12461
12462        let (src_delta, scene_delta) = frontend
12463            .edit_distance_constraint_label_position(
12464                &mock_ctx,
12465                version,
12466                sketch_id,
12467                constraint_id,
12468                label_position.clone(),
12469                vec![],
12470            )
12471            .await
12472            .unwrap();
12473        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12474
12475        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12476        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12477            panic!("Expected constraint object");
12478        };
12479        let Constraint::Radius(radius) = constraint else {
12480            panic!("Expected radius constraint");
12481        };
12482        assert_eq!(radius.label_position, Some(label_position));
12483
12484        mock_ctx.close().await;
12485    }
12486
12487    #[tokio::test(flavor = "multi_thread")]
12488    async fn test_vertical_distance_two_points() {
12489        let initial_source = "\
12490sketch(on = XY) {
12491  point(at = [var 1, var 2])
12492  point(at = [var 3, var 4])
12493}
12494";
12495
12496        let program = Program::parse(initial_source).unwrap().0.unwrap();
12497
12498        let mut frontend = FrontendState::new();
12499
12500        let mock_ctx = ExecutorContext::new_mock(None).await;
12501        let version = Version(0);
12502
12503        frontend.program = program.clone();
12504        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12505        frontend.update_state_after_exec(outcome, true);
12506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12507        let sketch_id = sketch_object.id;
12508        let sketch = expect_sketch(sketch_object);
12509        let point0_id = *sketch.segments.first().unwrap();
12510        let point1_id = *sketch.segments.get(1).unwrap();
12511        let label_position = Point2d {
12512            x: Number {
12513                value: 10.0,
12514                units: NumericSuffix::Mm,
12515            },
12516            y: Number {
12517                value: 11.0,
12518                units: NumericSuffix::Mm,
12519            },
12520        };
12521
12522        let constraint = Constraint::VerticalDistance(Distance {
12523            segments: vec![point0_id.into(), point1_id.into()],
12524            distance: Number {
12525                value: 2.0,
12526                units: NumericSuffix::Mm,
12527            },
12528            label_position: Some(label_position.clone()),
12529            source: Default::default(),
12530        });
12531        let (src_delta, scene_delta) = frontend
12532            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12533            .await
12534            .unwrap();
12535        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12536        assert_eq!(
12537            scene_delta.new_graph.objects.len(),
12538            5,
12539            "{:#?}",
12540            scene_delta.new_graph.objects
12541        );
12542        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12543        let sketch = expect_sketch(sketch_object);
12544        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12545        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12546            panic!("Expected constraint object");
12547        };
12548        let Constraint::VerticalDistance(distance) = constraint else {
12549            panic!("Expected vertical distance constraint");
12550        };
12551        assert_eq!(distance.label_position, Some(label_position));
12552
12553        mock_ctx.close().await;
12554    }
12555
12556    #[tokio::test(flavor = "multi_thread")]
12557    async fn test_add_fixed_standalone_point() {
12558        let initial_source = "\
12559sketch(on = XY) {
12560  point(at = [var 1, var 2])
12561}
12562";
12563
12564        let program = Program::parse(initial_source).unwrap().0.unwrap();
12565
12566        let mut frontend = FrontendState::new();
12567
12568        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12569        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12570        let version = Version(0);
12571
12572        frontend.hack_set_program(&ctx, program).await.unwrap();
12573        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12574        let sketch_id = sketch_object.id;
12575        let sketch = expect_sketch(sketch_object);
12576        let point_id = *sketch.segments.first().unwrap();
12577
12578        let (src_delta, scene_delta) = frontend
12579            .add_constraint(
12580                &mock_ctx,
12581                version,
12582                sketch_id,
12583                Constraint::Fixed(Fixed {
12584                    points: vec![FixedPoint {
12585                        point: point_id,
12586                        position: Point2d {
12587                            x: Number {
12588                                value: 2.0,
12589                                units: NumericSuffix::Mm,
12590                            },
12591                            y: Number {
12592                                value: 3.0,
12593                                units: NumericSuffix::Mm,
12594                            },
12595                        },
12596                    }],
12597                }),
12598            )
12599            .await
12600            .unwrap();
12601        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12602        assert_eq!(
12603            scene_delta.new_graph.objects.len(),
12604            4,
12605            "{:#?}",
12606            scene_delta.new_graph.objects
12607        );
12608
12609        ctx.close().await;
12610        mock_ctx.close().await;
12611    }
12612
12613    #[tokio::test(flavor = "multi_thread")]
12614    async fn test_add_fixed_multiple_points() {
12615        let initial_source = "\
12616sketch(on = XY) {
12617  point(at = [var 1, var 2])
12618  point(at = [var 3, var 4])
12619}
12620";
12621
12622        let program = Program::parse(initial_source).unwrap().0.unwrap();
12623
12624        let mut frontend = FrontendState::new();
12625
12626        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12627        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12628        let version = Version(0);
12629
12630        frontend.hack_set_program(&ctx, program).await.unwrap();
12631        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12632        let sketch_id = sketch_object.id;
12633        let sketch = expect_sketch(sketch_object);
12634        let point0_id = *sketch.segments.first().unwrap();
12635        let point1_id = *sketch.segments.get(1).unwrap();
12636
12637        let (src_delta, scene_delta) = frontend
12638            .add_constraint(
12639                &mock_ctx,
12640                version,
12641                sketch_id,
12642                Constraint::Fixed(Fixed {
12643                    points: vec![
12644                        FixedPoint {
12645                            point: point0_id,
12646                            position: Point2d {
12647                                x: Number {
12648                                    value: 2.0,
12649                                    units: NumericSuffix::Mm,
12650                                },
12651                                y: Number {
12652                                    value: 3.0,
12653                                    units: NumericSuffix::Mm,
12654                                },
12655                            },
12656                        },
12657                        FixedPoint {
12658                            point: point1_id,
12659                            position: Point2d {
12660                                x: Number {
12661                                    value: 4.0,
12662                                    units: NumericSuffix::Mm,
12663                                },
12664                                y: Number {
12665                                    value: 5.0,
12666                                    units: NumericSuffix::Mm,
12667                                },
12668                            },
12669                        },
12670                    ],
12671                }),
12672            )
12673            .await
12674            .unwrap();
12675        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12676        assert_eq!(
12677            scene_delta.new_graph.objects.len(),
12678            6,
12679            "{:#?}",
12680            scene_delta.new_graph.objects
12681        );
12682
12683        ctx.close().await;
12684        mock_ctx.close().await;
12685    }
12686
12687    #[tokio::test(flavor = "multi_thread")]
12688    async fn test_add_fixed_owned_point() {
12689        let initial_source = "\
12690sketch(on = XY) {
12691  line(start = [var 1, var 2], end = [var 3, var 4])
12692}
12693";
12694
12695        let program = Program::parse(initial_source).unwrap().0.unwrap();
12696
12697        let mut frontend = FrontendState::new();
12698
12699        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12700        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12701        let version = Version(0);
12702
12703        frontend.hack_set_program(&ctx, program).await.unwrap();
12704        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12705        let sketch_id = sketch_object.id;
12706        let sketch = expect_sketch(sketch_object);
12707        let line_start_id = *sketch.segments.first().unwrap();
12708
12709        let (src_delta, scene_delta) = frontend
12710            .add_constraint(
12711                &mock_ctx,
12712                version,
12713                sketch_id,
12714                Constraint::Fixed(Fixed {
12715                    points: vec![FixedPoint {
12716                        point: line_start_id,
12717                        position: Point2d {
12718                            x: Number {
12719                                value: 2.0,
12720                                units: NumericSuffix::Mm,
12721                            },
12722                            y: Number {
12723                                value: 3.0,
12724                                units: NumericSuffix::Mm,
12725                            },
12726                        },
12727                    }],
12728                }),
12729            )
12730            .await
12731            .unwrap();
12732        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12733        assert_eq!(
12734            scene_delta.new_graph.objects.len(),
12735            6,
12736            "{:#?}",
12737            scene_delta.new_graph.objects
12738        );
12739
12740        ctx.close().await;
12741        mock_ctx.close().await;
12742    }
12743
12744    #[tokio::test(flavor = "multi_thread")]
12745    async fn test_radius_error_cases() {
12746        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12747        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12748        let version = Version(0);
12749
12750        // Test: Single point should error
12751        let initial_source_point = "\
12752sketch(on = XY) {
12753  point(at = [var 1, var 2])
12754}
12755";
12756        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12757        let mut frontend_point = FrontendState::new();
12758        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12759        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12760        let sketch_id_point = sketch_object_point.id;
12761        let sketch_point = expect_sketch(sketch_object_point);
12762        let point_id = *sketch_point.segments.first().unwrap();
12763
12764        let constraint_point = Constraint::Radius(Radius {
12765            arc: point_id,
12766            radius: Number {
12767                value: 5.0,
12768                units: NumericSuffix::Mm,
12769            },
12770            label_position: None,
12771            source: Default::default(),
12772        });
12773        let result_point = frontend_point
12774            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12775            .await;
12776        assert!(result_point.is_err(), "Single point should error for radius");
12777
12778        // Test: Single line segment should error (only arc segments supported)
12779        let initial_source_line = "\
12780sketch(on = XY) {
12781  line(start = [var 1, var 2], end = [var 3, var 4])
12782}
12783";
12784        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12785        let mut frontend_line = FrontendState::new();
12786        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12787        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12788        let sketch_id_line = sketch_object_line.id;
12789        let sketch_line = expect_sketch(sketch_object_line);
12790        let line_id = *sketch_line.segments.first().unwrap();
12791
12792        let constraint_line = Constraint::Radius(Radius {
12793            arc: line_id,
12794            radius: Number {
12795                value: 5.0,
12796                units: NumericSuffix::Mm,
12797            },
12798            label_position: None,
12799            source: Default::default(),
12800        });
12801        let result_line = frontend_line
12802            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12803            .await;
12804        assert!(result_line.is_err(), "Single line segment should error for radius");
12805
12806        ctx.close().await;
12807        mock_ctx.close().await;
12808    }
12809
12810    #[tokio::test(flavor = "multi_thread")]
12811    async fn test_diameter_single_arc_segment() {
12812        let initial_source = "\
12813sketch(on = XY) {
12814  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12815}
12816";
12817
12818        let program = Program::parse(initial_source).unwrap().0.unwrap();
12819
12820        let mut frontend = FrontendState::new();
12821
12822        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12823        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
12824        let version = Version(0);
12825
12826        frontend.hack_set_program(&ctx, program).await.unwrap();
12827        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12828        let sketch_id = sketch_object.id;
12829        let sketch = expect_sketch(sketch_object);
12830        // Find the arc segment (not the points)
12831        let arc_id = sketch
12832            .segments
12833            .iter()
12834            .find(|&seg_id| {
12835                let obj = frontend.scene_graph.objects.get(seg_id.0);
12836                matches!(
12837                    obj.map(|o| &o.kind),
12838                    Some(ObjectKind::Segment {
12839                        segment: Segment::Arc(_)
12840                    })
12841                )
12842            })
12843            .unwrap();
12844
12845        let constraint = Constraint::Diameter(Diameter {
12846            arc: *arc_id,
12847            diameter: Number {
12848                value: 10.0,
12849                units: NumericSuffix::Mm,
12850            },
12851            label_position: None,
12852            source: Default::default(),
12853        });
12854        let (src_delta, scene_delta) = frontend
12855            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12856            .await
12857            .unwrap();
12858        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12859        assert_eq!(
12860            scene_delta.new_graph.objects.len(),
12861            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12862            "{:#?}",
12863            scene_delta.new_graph.objects
12864        );
12865
12866        ctx.close().await;
12867        mock_ctx.close().await;
12868    }
12869
12870    #[tokio::test(flavor = "multi_thread")]
12871    async fn test_diameter_single_arc_segment_with_label_position() {
12872        let initial_source = "\
12873sketch(on = XY) {
12874  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12875}
12876";
12877
12878        let program = Program::parse(initial_source).unwrap().0.unwrap();
12879        let mut frontend = FrontendState::new();
12880        let mock_ctx = ExecutorContext::new_mock(None).await;
12881        let version = Version(0);
12882
12883        frontend.program = program.clone();
12884        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12885        frontend.update_state_after_exec(outcome, true);
12886        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12887        let sketch_id = sketch_object.id;
12888        let sketch = expect_sketch(sketch_object);
12889        let arc_id = sketch
12890            .segments
12891            .iter()
12892            .find(|&seg_id| {
12893                let obj = frontend.scene_graph.objects.get(seg_id.0);
12894                matches!(
12895                    obj.map(|o| &o.kind),
12896                    Some(ObjectKind::Segment {
12897                        segment: Segment::Arc(_)
12898                    })
12899                )
12900            })
12901            .unwrap();
12902
12903        let label_position = Point2d {
12904            x: Number {
12905                value: 10.0,
12906                units: NumericSuffix::Mm,
12907            },
12908            y: Number {
12909                value: 11.0,
12910                units: NumericSuffix::Mm,
12911            },
12912        };
12913        let constraint = Constraint::Diameter(Diameter {
12914            arc: *arc_id,
12915            diameter: Number {
12916                value: 10.0,
12917                units: NumericSuffix::Mm,
12918            },
12919            label_position: Some(label_position.clone()),
12920            source: Default::default(),
12921        });
12922        let (src_delta, scene_delta) = frontend
12923            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12924            .await
12925            .unwrap();
12926        insta::assert_snapshot!(
12927            "test_diameter_single_arc_segment_with_label_position",
12928            src_delta.text.as_str()
12929        );
12930
12931        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12932        let sketch = expect_sketch(sketch_object);
12933        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12934        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12935            panic!("Expected constraint object");
12936        };
12937        let Constraint::Diameter(diameter) = constraint else {
12938            panic!("Expected diameter constraint");
12939        };
12940        assert_eq!(diameter.label_position, Some(label_position));
12941
12942        mock_ctx.close().await;
12943    }
12944
12945    #[tokio::test(flavor = "multi_thread")]
12946    async fn test_edit_diameter_constraint_label_position() {
12947        let initial_source = "\
12948sketch(on = XY) {
12949  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12950  diameter(arc1) == 10mm
12951}
12952";
12953
12954        let program = Program::parse(initial_source).unwrap().0.unwrap();
12955        let mut frontend = FrontendState::new();
12956        let mock_ctx = ExecutorContext::new_mock(None).await;
12957        let version = Version(0);
12958
12959        frontend.program = program.clone();
12960        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12961        frontend.update_state_after_exec(outcome, true);
12962        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12963        let sketch_id = sketch_object.id;
12964        let sketch = expect_sketch(sketch_object);
12965        let constraint_id = sketch.constraints[0];
12966        let label_position = Point2d {
12967            x: Number {
12968                value: 10.0,
12969                units: NumericSuffix::Mm,
12970            },
12971            y: Number {
12972                value: 11.0,
12973                units: NumericSuffix::Mm,
12974            },
12975        };
12976
12977        let (src_delta, scene_delta) = frontend
12978            .edit_distance_constraint_label_position(
12979                &mock_ctx,
12980                version,
12981                sketch_id,
12982                constraint_id,
12983                label_position.clone(),
12984                vec![],
12985            )
12986            .await
12987            .unwrap();
12988        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12989
12990        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12991        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12992            panic!("Expected constraint object");
12993        };
12994        let Constraint::Diameter(diameter) = constraint else {
12995            panic!("Expected diameter constraint");
12996        };
12997        assert_eq!(diameter.label_position, Some(label_position));
12998
12999        mock_ctx.close().await;
13000    }
13001
13002    #[tokio::test(flavor = "multi_thread")]
13003    async fn test_diameter_error_cases() {
13004        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13005        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13006        let version = Version(0);
13007
13008        // Test: Single point should error
13009        let initial_source_point = "\
13010sketch(on = XY) {
13011  point(at = [var 1, var 2])
13012}
13013";
13014        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13015        let mut frontend_point = FrontendState::new();
13016        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13017        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13018        let sketch_id_point = sketch_object_point.id;
13019        let sketch_point = expect_sketch(sketch_object_point);
13020        let point_id = *sketch_point.segments.first().unwrap();
13021
13022        let constraint_point = Constraint::Diameter(Diameter {
13023            arc: point_id,
13024            diameter: Number {
13025                value: 10.0,
13026                units: NumericSuffix::Mm,
13027            },
13028            label_position: None,
13029            source: Default::default(),
13030        });
13031        let result_point = frontend_point
13032            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13033            .await;
13034        assert!(result_point.is_err(), "Single point should error for diameter");
13035
13036        // Test: Single line segment should error (only arc segments supported)
13037        let initial_source_line = "\
13038sketch(on = XY) {
13039  line(start = [var 1, var 2], end = [var 3, var 4])
13040}
13041";
13042        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13043        let mut frontend_line = FrontendState::new();
13044        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13045        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13046        let sketch_id_line = sketch_object_line.id;
13047        let sketch_line = expect_sketch(sketch_object_line);
13048        let line_id = *sketch_line.segments.first().unwrap();
13049
13050        let constraint_line = Constraint::Diameter(Diameter {
13051            arc: line_id,
13052            diameter: Number {
13053                value: 10.0,
13054                units: NumericSuffix::Mm,
13055            },
13056            label_position: None,
13057            source: Default::default(),
13058        });
13059        let result_line = frontend_line
13060            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13061            .await;
13062        assert!(result_line.is_err(), "Single line segment should error for diameter");
13063
13064        ctx.close().await;
13065        mock_ctx.close().await;
13066    }
13067
13068    #[tokio::test(flavor = "multi_thread")]
13069    async fn test_line_horizontal() {
13070        let initial_source = "\
13071sketch(on = XY) {
13072  line(start = [var 1, var 2], end = [var 3, var 4])
13073}
13074";
13075
13076        let program = Program::parse(initial_source).unwrap().0.unwrap();
13077
13078        let mut frontend = FrontendState::new();
13079
13080        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13081        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13082        let version = Version(0);
13083
13084        frontend.hack_set_program(&ctx, program).await.unwrap();
13085        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13086        let sketch_id = sketch_object.id;
13087        let sketch = expect_sketch(sketch_object);
13088        let line1_id = *sketch.segments.get(2).unwrap();
13089
13090        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13091        let (src_delta, scene_delta) = frontend
13092            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13093            .await
13094            .unwrap();
13095        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13096        assert_eq!(
13097            scene_delta.new_graph.objects.len(),
13098            6,
13099            "{:#?}",
13100            scene_delta.new_graph.objects
13101        );
13102
13103        ctx.close().await;
13104        mock_ctx.close().await;
13105    }
13106
13107    #[tokio::test(flavor = "multi_thread")]
13108    async fn test_control_point_spline_edge_horizontal() {
13109        let initial_source = "\
13110@settings(experimentalFeatures = allow)
13111splineSketch = sketch(on = XY) {
13112  controlPointSpline1 = controlPointSpline(points = [
13113    [var 0mm, var 0mm],
13114    [var 10mm, var 20mm],
13115    [var 20mm, var 0mm],
13116  ])
13117}
13118";
13119
13120        let program = Program::parse(initial_source).unwrap().0.unwrap();
13121
13122        let mut frontend = FrontendState::new();
13123
13124        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13125        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13126        let version = Version(0);
13127
13128        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13130        let sketch_id = sketch_object.id;
13131        let sketch = expect_sketch(sketch_object);
13132        let spline_id = sketch
13133            .segments
13134            .iter()
13135            .copied()
13136            .find(|seg_id| {
13137                matches!(
13138                    &frontend.scene_graph.objects[seg_id.0].kind,
13139                    ObjectKind::Segment {
13140                        segment: Segment::ControlPointSpline(_)
13141                    }
13142                )
13143            })
13144            .expect("Expected a control point spline segment in sketch");
13145        let edge_id = frontend
13146            .scene_graph
13147            .objects
13148            .iter()
13149            .find_map(|obj| match &obj.kind {
13150                ObjectKind::Segment {
13151                    segment: Segment::Line(line),
13152                } if line.owner == Some(spline_id) => Some(obj.id),
13153                _ => None,
13154            })
13155            .expect("Expected an owned control-polygon edge");
13156
13157        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13158        let (src_delta, _) = frontend
13159            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13160            .await
13161            .unwrap();
13162        assert!(
13163            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13164            "Expected horizontal constraint on spline edge, got: {}",
13165            src_delta.text
13166        );
13167
13168        ctx.close().await;
13169        mock_ctx.close().await;
13170    }
13171
13172    #[tokio::test(flavor = "multi_thread")]
13173    async fn test_control_point_spline_edge_angle() {
13174        let initial_source = "\
13175@settings(experimentalFeatures = allow)
13176splineSketch = sketch(on = XY) {
13177  controlPointSpline1 = controlPointSpline(points = [
13178    [var 0mm, var 0mm],
13179    [var 10mm, var 20mm],
13180    [var 20mm, var 0mm],
13181  ])
13182
13183  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13184}
13185";
13186
13187        let program = Program::parse(initial_source).unwrap().0.unwrap();
13188
13189        let mut frontend = FrontendState::new();
13190
13191        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13192        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13193        let version = Version(0);
13194
13195        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13196        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13197        let sketch_id = sketch_object.id;
13198        let sketch = expect_sketch(sketch_object);
13199        let spline_id = sketch
13200            .segments
13201            .iter()
13202            .copied()
13203            .find(|seg_id| {
13204                matches!(
13205                    &frontend.scene_graph.objects[seg_id.0].kind,
13206                    ObjectKind::Segment {
13207                        segment: Segment::ControlPointSpline(_)
13208                    }
13209                )
13210            })
13211            .expect("Expected a control point spline segment in sketch");
13212        let edge_id = frontend
13213            .scene_graph
13214            .objects
13215            .iter()
13216            .find_map(|obj| match &obj.kind {
13217                ObjectKind::Segment {
13218                    segment: Segment::Line(line),
13219                } if line.owner == Some(spline_id) => Some(obj.id),
13220                _ => None,
13221            })
13222            .expect("Expected an owned control-polygon edge");
13223        let line1_id = frontend
13224            .scene_graph
13225            .objects
13226            .iter()
13227            .find_map(|obj| match &obj.kind {
13228                ObjectKind::Segment {
13229                    segment: Segment::Line(line),
13230                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13231                _ => None,
13232            })
13233            .or_else(|| {
13234                sketch.segments.iter().copied().find(|seg_id| {
13235                    matches!(
13236                        &frontend.scene_graph.objects[seg_id.0].kind,
13237                        ObjectKind::Segment {
13238                            segment: Segment::Line(line),
13239                        } if line.owner.is_none()
13240                    )
13241                })
13242            })
13243            .expect("Expected a standalone line segment in sketch");
13244
13245        let constraint = Constraint::Angle(Angle {
13246            lines: vec![line1_id, edge_id],
13247            angle: Number {
13248                value: 30.0,
13249                units: NumericSuffix::Deg,
13250            },
13251            sector: None,
13252            inverse: None,
13253            label_position: None,
13254            source: Default::default(),
13255        });
13256        let (src_delta, _) = frontend
13257            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13258            .await
13259            .unwrap();
13260        assert!(
13261            src_delta
13262                .text
13263                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13264            "Expected angle constraint on spline edge, got: {}",
13265            src_delta.text
13266        );
13267
13268        ctx.close().await;
13269        mock_ctx.close().await;
13270    }
13271
13272    #[tokio::test(flavor = "multi_thread")]
13273    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13274        let initial_source = "\
13275@settings(experimentalFeatures = allow)
13276splineSketch = sketch(on = XY) {
13277  spline1 = controlPointSpline(points = [
13278    [var 0mm, var 0mm],
13279    [var 10mm, var 20mm],
13280    [var 20mm, var 0mm],
13281  ])
13282  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13283  coincident([spline1.controls[1], spline1.edges[0]])
13284  coincident([spline1.controls[0], line1])
13285}
13286";
13287
13288        let program = Program::parse(initial_source).unwrap().0.unwrap();
13289
13290        let mut frontend = FrontendState::new();
13291
13292        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13293        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13294
13295        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13296
13297        let ui_scene_graph = frontend.scene_graph_for_ui();
13298        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13299        let sketch = expect_sketch(sketch_object);
13300
13301        assert_eq!(
13302            sketch.constraints.len(),
13303            1,
13304            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13305        );
13306
13307        let visible_constraints = ui_scene_graph
13308            .objects
13309            .iter()
13310            .filter_map(|object| match &object.kind {
13311                ObjectKind::Constraint {
13312                    constraint: Constraint::Coincident(coincident),
13313                } => Some(coincident.clone()),
13314                _ => None,
13315            })
13316            .collect::<Vec<_>>();
13317
13318        assert_eq!(
13319            visible_constraints.len(),
13320            1,
13321            "Expected only one coincident constraint object in the UI scene graph"
13322        );
13323        assert_eq!(
13324            visible_constraints[0].get_segments().len(),
13325            2,
13326            "Expected the remaining visible coincident constraint to reference two segments"
13327        );
13328
13329        ctx.close().await;
13330        mock_ctx.close().await;
13331    }
13332
13333    #[tokio::test(flavor = "multi_thread")]
13334    async fn test_edit_control_point_spline_can_append_control_point() {
13335        let initial_source = "\
13336@settings(experimentalFeatures = allow)
13337splineSketch = sketch(on = XY) {
13338  controlPointSpline(points = [
13339    [var 0mm, var 0mm],
13340    [var 10mm, var 20mm],
13341    [var 20mm, var 0mm],
13342  ])
13343}
13344";
13345
13346        let program = Program::parse(initial_source).unwrap().0.unwrap();
13347
13348        let mut frontend = FrontendState::new();
13349
13350        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13351        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13352        let version = Version(0);
13353
13354        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13355        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13356        let sketch_id = sketch_object.id;
13357        let sketch = expect_sketch(sketch_object);
13358        let spline_id = sketch
13359            .segments
13360            .iter()
13361            .copied()
13362            .find(|seg_id| {
13363                matches!(
13364                    &frontend.scene_graph.objects[seg_id.0].kind,
13365                    ObjectKind::Segment {
13366                        segment: Segment::ControlPointSpline(_)
13367                    }
13368                )
13369            })
13370            .expect("Expected a control point spline segment in sketch");
13371
13372        let ctor = ControlPointSplineCtor {
13373            points: vec![
13374                Point2d {
13375                    x: Expr::Var(Number {
13376                        value: 0.0,
13377                        units: NumericSuffix::Mm,
13378                    }),
13379                    y: Expr::Var(Number {
13380                        value: 0.0,
13381                        units: NumericSuffix::Mm,
13382                    }),
13383                },
13384                Point2d {
13385                    x: Expr::Var(Number {
13386                        value: 10.0,
13387                        units: NumericSuffix::Mm,
13388                    }),
13389                    y: Expr::Var(Number {
13390                        value: 20.0,
13391                        units: NumericSuffix::Mm,
13392                    }),
13393                },
13394                Point2d {
13395                    x: Expr::Var(Number {
13396                        value: 20.0,
13397                        units: NumericSuffix::Mm,
13398                    }),
13399                    y: Expr::Var(Number {
13400                        value: 0.0,
13401                        units: NumericSuffix::Mm,
13402                    }),
13403                },
13404                Point2d {
13405                    x: Expr::Var(Number {
13406                        value: 30.0,
13407                        units: NumericSuffix::Mm,
13408                    }),
13409                    y: Expr::Var(Number {
13410                        value: 10.0,
13411                        units: NumericSuffix::Mm,
13412                    }),
13413                },
13414            ],
13415            construction: None,
13416        };
13417
13418        let segments = vec![ExistingSegmentCtor {
13419            id: spline_id,
13420            ctor: SegmentCtor::ControlPointSpline(ctor),
13421        }];
13422        let (src_delta, scene_delta) = frontend
13423            .edit_segments(&mock_ctx, version, sketch_id, segments)
13424            .await
13425            .unwrap();
13426
13427        assert!(
13428            src_delta.text.contains("[var 30mm, var 10mm]"),
13429            "Expected appended spline control point in source, got: {}",
13430            src_delta.text
13431        );
13432
13433        assert!(
13434            scene_delta.invalidates_ids,
13435            "Expected appending a spline control point to invalidate ids"
13436        );
13437        let updated_spline = scene_delta
13438            .new_graph
13439            .objects
13440            .iter()
13441            .find_map(|obj| match &obj.kind {
13442                ObjectKind::Segment {
13443                    segment: Segment::ControlPointSpline(updated_spline),
13444                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13445                _ => None,
13446            })
13447            .expect("Expected edited scene graph to contain a four-point control point spline");
13448        assert_eq!(
13449            updated_spline.controls.len(),
13450            4,
13451            "Expected edited spline to expose four control points"
13452        );
13453
13454        ctx.close().await;
13455        mock_ctx.close().await;
13456    }
13457
13458    #[tokio::test(flavor = "multi_thread")]
13459    async fn test_line_vertical() {
13460        let initial_source = "\
13461sketch(on = XY) {
13462  line(start = [var 1, var 2], end = [var 3, var 4])
13463}
13464";
13465
13466        let program = Program::parse(initial_source).unwrap().0.unwrap();
13467
13468        let mut frontend = FrontendState::new();
13469
13470        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13471        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13472        let version = Version(0);
13473
13474        frontend.hack_set_program(&ctx, program).await.unwrap();
13475        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13476        let sketch_id = sketch_object.id;
13477        let sketch = expect_sketch(sketch_object);
13478        let line1_id = *sketch.segments.get(2).unwrap();
13479
13480        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13481        let (src_delta, scene_delta) = frontend
13482            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13483            .await
13484            .unwrap();
13485        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13486        assert_eq!(
13487            scene_delta.new_graph.objects.len(),
13488            6,
13489            "{:#?}",
13490            scene_delta.new_graph.objects
13491        );
13492
13493        ctx.close().await;
13494        mock_ctx.close().await;
13495    }
13496
13497    #[tokio::test(flavor = "multi_thread")]
13498    async fn test_points_vertical() {
13499        let initial_source = "\
13500sketch001 = sketch(on = XY) {
13501  p0 = point(at = [var -2.23mm, var 3.1mm])
13502  pf = point(at = [4, 4])
13503}
13504";
13505
13506        let program = Program::parse(initial_source).unwrap().0.unwrap();
13507
13508        let mut frontend = FrontendState::new();
13509
13510        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13511        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13512        let version = Version(0);
13513
13514        frontend.hack_set_program(&ctx, program).await.unwrap();
13515        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13516        let sketch_id = sketch_object.id;
13517        let sketch = expect_sketch(sketch_object);
13518        let point_ids = vec![
13519            sketch.segments.first().unwrap().to_owned(),
13520            sketch.segments.get(1).unwrap().to_owned(),
13521        ];
13522
13523        let constraint = Constraint::Vertical(Vertical::Points {
13524            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13525        });
13526        let (src_delta, scene_delta) = frontend
13527            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13528            .await
13529            .unwrap();
13530        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13531        assert_eq!(
13532            scene_delta.new_graph.objects.len(),
13533            5,
13534            "{:#?}",
13535            scene_delta.new_graph.objects
13536        );
13537
13538        ctx.close().await;
13539        mock_ctx.close().await;
13540    }
13541
13542    #[tokio::test(flavor = "multi_thread")]
13543    async fn test_points_horizontal() {
13544        let initial_source = "\
13545sketch001 = sketch(on = XY) {
13546  p0 = point(at = [var -2.23mm, var 3.1mm])
13547  pf = point(at = [4, 4])
13548}
13549";
13550
13551        let program = Program::parse(initial_source).unwrap().0.unwrap();
13552
13553        let mut frontend = FrontendState::new();
13554
13555        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13556        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13557        let version = Version(0);
13558
13559        frontend.hack_set_program(&ctx, program).await.unwrap();
13560        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13561        let sketch_id = sketch_object.id;
13562        let sketch = expect_sketch(sketch_object);
13563        let point_ids = vec![
13564            sketch.segments.first().unwrap().to_owned(),
13565            sketch.segments.get(1).unwrap().to_owned(),
13566        ];
13567
13568        let constraint = Constraint::Horizontal(Horizontal::Points {
13569            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13570        });
13571        let (src_delta, scene_delta) = frontend
13572            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13573            .await
13574            .unwrap();
13575        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13576        assert_eq!(
13577            scene_delta.new_graph.objects.len(),
13578            5,
13579            "{:#?}",
13580            scene_delta.new_graph.objects
13581        );
13582
13583        ctx.close().await;
13584        mock_ctx.close().await;
13585    }
13586
13587    #[tokio::test(flavor = "multi_thread")]
13588    async fn test_point_horizontal_with_origin() {
13589        let initial_source = "\
13590sketch001 = sketch(on = XY) {
13591  p0 = point(at = [var -2.23mm, var 3.1mm])
13592}
13593";
13594
13595        let program = Program::parse(initial_source).unwrap().0.unwrap();
13596
13597        let mut frontend = FrontendState::new();
13598
13599        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13600        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13601        let version = Version(0);
13602
13603        frontend.hack_set_program(&ctx, program).await.unwrap();
13604        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13605        let sketch_id = sketch_object.id;
13606        let sketch = expect_sketch(sketch_object);
13607        let point_id = *sketch.segments.first().unwrap();
13608
13609        let constraint = Constraint::Horizontal(Horizontal::Points {
13610            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13611        });
13612        let (src_delta, scene_delta) = frontend
13613            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13614            .await
13615            .unwrap();
13616        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13617        assert_eq!(
13618            scene_delta.new_graph.objects.len(),
13619            4,
13620            "{:#?}",
13621            scene_delta.new_graph.objects
13622        );
13623
13624        ctx.close().await;
13625        mock_ctx.close().await;
13626    }
13627
13628    #[tokio::test(flavor = "multi_thread")]
13629    async fn test_lines_equal_length() {
13630        let initial_source = "\
13631sketch(on = XY) {
13632  line(start = [var 1, var 2], end = [var 3, var 4])
13633  line(start = [var 5, var 6], end = [var 7, var 8])
13634}
13635";
13636
13637        let program = Program::parse(initial_source).unwrap().0.unwrap();
13638
13639        let mut frontend = FrontendState::new();
13640
13641        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13642        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13643        let version = Version(0);
13644
13645        frontend.hack_set_program(&ctx, program).await.unwrap();
13646        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13647        let sketch_id = sketch_object.id;
13648        let sketch = expect_sketch(sketch_object);
13649        let line1_id = *sketch.segments.get(2).unwrap();
13650        let line2_id = *sketch.segments.get(5).unwrap();
13651
13652        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13653            lines: vec![line1_id, line2_id],
13654        });
13655        let (src_delta, scene_delta) = frontend
13656            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13657            .await
13658            .unwrap();
13659        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13660        assert_eq!(
13661            scene_delta.new_graph.objects.len(),
13662            9,
13663            "{:#?}",
13664            scene_delta.new_graph.objects
13665        );
13666
13667        ctx.close().await;
13668        mock_ctx.close().await;
13669    }
13670
13671    #[tokio::test(flavor = "multi_thread")]
13672    async fn test_add_constraint_multi_line_equal_length() {
13673        let initial_source = "\
13674sketch(on = XY) {
13675  line(start = [var 1, var 2], end = [var 3, var 4])
13676  line(start = [var 5, var 6], end = [var 7, var 8])
13677  line(start = [var 9, var 10], end = [var 11, var 12])
13678}
13679";
13680
13681        let program = Program::parse(initial_source).unwrap().0.unwrap();
13682
13683        let mut frontend = FrontendState::new();
13684        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13685        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13686        let version = Version(0);
13687
13688        frontend.hack_set_program(&ctx, program).await.unwrap();
13689        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13690        let sketch_id = sketch_object.id;
13691        let sketch = expect_sketch(sketch_object);
13692        let line1_id = *sketch.segments.get(2).unwrap();
13693        let line2_id = *sketch.segments.get(5).unwrap();
13694        let line3_id = *sketch.segments.get(8).unwrap();
13695
13696        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13697            lines: vec![line1_id, line2_id, line3_id],
13698        });
13699        let (src_delta, scene_delta) = frontend
13700            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13701            .await
13702            .unwrap();
13703        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13704        let constraints = scene_delta
13705            .new_graph
13706            .objects
13707            .iter()
13708            .filter_map(|obj| {
13709                let ObjectKind::Constraint { constraint } = &obj.kind else {
13710                    return None;
13711                };
13712                Some(constraint)
13713            })
13714            .collect::<Vec<_>>();
13715
13716        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13717        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13718            panic!("expected equal length constraint, got {:?}", constraints[0]);
13719        };
13720        assert_eq!(lines_equal_length.lines.len(), 3);
13721
13722        ctx.close().await;
13723        mock_ctx.close().await;
13724    }
13725
13726    #[tokio::test(flavor = "multi_thread")]
13727    async fn test_lines_parallel() {
13728        let initial_source = "\
13729sketch(on = XY) {
13730  line(start = [var 1, var 2], end = [var 3, var 4])
13731  line(start = [var 5, var 6], end = [var 7, var 8])
13732}
13733";
13734
13735        let program = Program::parse(initial_source).unwrap().0.unwrap();
13736
13737        let mut frontend = FrontendState::new();
13738
13739        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13740        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13741        let version = Version(0);
13742
13743        frontend.hack_set_program(&ctx, program).await.unwrap();
13744        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13745        let sketch_id = sketch_object.id;
13746        let sketch = expect_sketch(sketch_object);
13747        let line1_id = *sketch.segments.get(2).unwrap();
13748        let line2_id = *sketch.segments.get(5).unwrap();
13749
13750        let constraint = Constraint::Parallel(Parallel {
13751            lines: vec![line1_id, line2_id],
13752        });
13753        let (src_delta, scene_delta) = frontend
13754            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13755            .await
13756            .unwrap();
13757        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13758        assert_eq!(
13759            scene_delta.new_graph.objects.len(),
13760            9,
13761            "{:#?}",
13762            scene_delta.new_graph.objects
13763        );
13764
13765        ctx.close().await;
13766        mock_ctx.close().await;
13767    }
13768
13769    #[tokio::test(flavor = "multi_thread")]
13770    async fn test_lines_parallel_multiline() {
13771        let initial_source = "\
13772sketch(on = XY) {
13773  line(start = [var 1, var 2], end = [var 3, var 4])
13774  line(start = [var 5, var 6], end = [var 7, var 8])
13775  line(start = [var 9, var 10], end = [var 11, var 12])
13776}
13777";
13778
13779        let program = Program::parse(initial_source).unwrap().0.unwrap();
13780
13781        let mut frontend = FrontendState::new();
13782
13783        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13784        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13785        let version = Version(0);
13786
13787        frontend.hack_set_program(&ctx, program).await.unwrap();
13788        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13789        let sketch_id = sketch_object.id;
13790        let sketch = expect_sketch(sketch_object);
13791        let line1_id = *sketch.segments.get(2).unwrap();
13792        let line2_id = *sketch.segments.get(5).unwrap();
13793        let line3_id = *sketch.segments.get(8).unwrap();
13794
13795        let constraint = Constraint::Parallel(Parallel {
13796            lines: vec![line1_id, line2_id, line3_id],
13797        });
13798        let (src_delta, scene_delta) = frontend
13799            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13800            .await
13801            .unwrap();
13802        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13803
13804        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13805        let sketch = expect_sketch(sketch_object);
13806        assert_eq!(sketch.constraints.len(), 1);
13807
13808        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13809        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13810            panic!("Expected constraint object");
13811        };
13812        let Constraint::Parallel(parallel) = constraint else {
13813            panic!("Expected parallel constraint");
13814        };
13815        assert_eq!(parallel.lines.len(), 3);
13816
13817        ctx.close().await;
13818        mock_ctx.close().await;
13819    }
13820
13821    #[tokio::test(flavor = "multi_thread")]
13822    async fn test_lines_perpendicular() {
13823        let initial_source = "\
13824sketch(on = XY) {
13825  line(start = [var 1, var 2], end = [var 3, var 4])
13826  line(start = [var 5, var 6], end = [var 7, var 8])
13827}
13828";
13829
13830        let program = Program::parse(initial_source).unwrap().0.unwrap();
13831
13832        let mut frontend = FrontendState::new();
13833
13834        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13835        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13836        let version = Version(0);
13837
13838        frontend.hack_set_program(&ctx, program).await.unwrap();
13839        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13840        let sketch_id = sketch_object.id;
13841        let sketch = expect_sketch(sketch_object);
13842        let line1_id = *sketch.segments.get(2).unwrap();
13843        let line2_id = *sketch.segments.get(5).unwrap();
13844
13845        let constraint = Constraint::Perpendicular(Perpendicular {
13846            lines: vec![line1_id, line2_id],
13847        });
13848        let (src_delta, scene_delta) = frontend
13849            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13850            .await
13851            .unwrap();
13852        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13853        assert_eq!(
13854            scene_delta.new_graph.objects.len(),
13855            9,
13856            "{:#?}",
13857            scene_delta.new_graph.objects
13858        );
13859
13860        ctx.close().await;
13861        mock_ctx.close().await;
13862    }
13863
13864    #[tokio::test(flavor = "multi_thread")]
13865    async fn test_lines_angle() {
13866        let initial_source = "\
13867sketch(on = XY) {
13868  line(start = [var 1, var 2], end = [var 3, var 4])
13869  line(start = [var 5, var 6], end = [var 7, var 8])
13870}
13871";
13872
13873        let program = Program::parse(initial_source).unwrap().0.unwrap();
13874
13875        let mut frontend = FrontendState::new();
13876
13877        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13878        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13879        let version = Version(0);
13880
13881        frontend.hack_set_program(&ctx, program).await.unwrap();
13882        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13883        let sketch_id = sketch_object.id;
13884        let sketch = expect_sketch(sketch_object);
13885        let line1_id = *sketch.segments.get(2).unwrap();
13886        let line2_id = *sketch.segments.get(5).unwrap();
13887
13888        let constraint = Constraint::Angle(Angle {
13889            lines: vec![line1_id, line2_id],
13890            angle: Number {
13891                value: 30.0,
13892                units: NumericSuffix::Deg,
13893            },
13894            sector: None,
13895            inverse: None,
13896            label_position: None,
13897            source: Default::default(),
13898        });
13899        let (src_delta, scene_delta) = frontend
13900            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13901            .await
13902            .unwrap();
13903        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13904        assert_eq!(
13905            scene_delta.new_graph.objects.len(),
13906            9,
13907            "{:#?}",
13908            scene_delta.new_graph.objects
13909        );
13910
13911        ctx.close().await;
13912        mock_ctx.close().await;
13913    }
13914
13915    #[tokio::test(flavor = "multi_thread")]
13916    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13917        let initial_source = "\
13918sketch(on = XY) {
13919  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13920  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13921}
13922";
13923
13924        let program = Program::parse(initial_source).unwrap().0.unwrap();
13925
13926        let mut frontend = FrontendState::new();
13927
13928        let mock_ctx = ExecutorContext::new_mock(None).await;
13929        let version = Version(0);
13930
13931        frontend.program = program.clone();
13932        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13933        frontend.update_state_after_exec(outcome, true);
13934        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13935        let sketch_id = sketch_object.id;
13936        let sketch = expect_sketch(sketch_object);
13937        let line1_id = *sketch.segments.get(2).unwrap();
13938        let line2_id = *sketch.segments.get(5).unwrap();
13939
13940        let constraint = Constraint::Angle(Angle {
13941            lines: vec![line1_id, line2_id],
13942            angle: Number {
13943                value: 270.0,
13944                units: NumericSuffix::Deg,
13945            },
13946            sector: Some(1),
13947            inverse: Some(true),
13948            label_position: Some(Point2d {
13949                x: Number {
13950                    value: -0.73,
13951                    units: NumericSuffix::Mm,
13952                },
13953                y: Number {
13954                    value: 0.75,
13955                    units: NumericSuffix::Mm,
13956                },
13957            }),
13958            source: Default::default(),
13959        });
13960        let (src_delta, _) = frontend
13961            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13962            .await
13963            .unwrap();
13964        assert_eq!(
13965            src_delta.text.as_str(),
13966            "\
13967sketch(on = XY) {
13968  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13969  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13970  angleDimension(
13971    lines = [line1, line2],
13972    sector = 1,
13973    inverse = true,
13974    labelPosition = [-0.73mm, 0.75mm],
13975  ) == 270deg
13976}
13977"
13978        );
13979
13980        mock_ctx.close().await;
13981    }
13982
13983    #[tokio::test(flavor = "multi_thread")]
13984    async fn test_segments_tangent() {
13985        let initial_source = "\
13986sketch(on = XY) {
13987  line(start = [var 1, var 2], end = [var 3, var 4])
13988  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13989}
13990";
13991
13992        let program = Program::parse(initial_source).unwrap().0.unwrap();
13993
13994        let mut frontend = FrontendState::new();
13995
13996        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13997        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
13998        let version = Version(0);
13999
14000        frontend.hack_set_program(&ctx, program).await.unwrap();
14001        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14002        let sketch_id = sketch_object.id;
14003        let sketch = expect_sketch(sketch_object);
14004        let line1_id = *sketch.segments.get(2).unwrap();
14005        let arc1_id = *sketch.segments.get(6).unwrap();
14006
14007        let constraint = Constraint::Tangent(Tangent {
14008            input: vec![line1_id, arc1_id],
14009        });
14010        let (src_delta, scene_delta) = frontend
14011            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14012            .await
14013            .unwrap();
14014        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14015        assert_eq!(
14016            scene_delta.new_graph.objects.len(),
14017            10,
14018            "{:#?}",
14019            scene_delta.new_graph.objects
14020        );
14021
14022        ctx.close().await;
14023        mock_ctx.close().await;
14024    }
14025
14026    #[tokio::test(flavor = "multi_thread")]
14027    async fn test_point_midpoint() {
14028        let initial_source = "\
14029sketch(on = XY) {
14030  point(at = [var 1, var 1])
14031  line(start = [var 0, var 0], end = [var 6, var 4])
14032}
14033";
14034
14035        let program = Program::parse(initial_source).unwrap().0.unwrap();
14036
14037        let mut frontend = FrontendState::new();
14038
14039        let ctx = ExecutorContext::new_mock(None).await;
14040        let version = Version(0);
14041
14042        frontend.program = program.clone();
14043        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14044        frontend.update_state_after_exec(outcome, true);
14045        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14046        let sketch_id = sketch_object.id;
14047        let sketch = expect_sketch(sketch_object);
14048        let point_id = *sketch.segments.first().unwrap();
14049        let line_id = *sketch.segments.get(3).unwrap();
14050
14051        let constraint = Constraint::Midpoint(Midpoint {
14052            point: ConstraintSegment::from(point_id),
14053            segment: line_id,
14054        });
14055        let (src_delta, scene_delta) = frontend
14056            .add_constraint(&ctx, version, sketch_id, constraint)
14057            .await
14058            .unwrap();
14059        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14060        assert_eq!(
14061            scene_delta.new_graph.objects.len(),
14062            7,
14063            "{:#?}",
14064            scene_delta.new_graph.objects
14065        );
14066
14067        ctx.close().await;
14068    }
14069
14070    #[tokio::test(flavor = "multi_thread")]
14071    async fn test_segments_symmetric() {
14072        let initial_source = "\
14073sketch(on = XY) {
14074  line(start = [var 0, var 0], end = [var 0, var 4])
14075  line(start = [var 4, var 0], end = [var 4, var 4])
14076  line(start = [var 2, var -1], end = [var 2, var 5])
14077}
14078";
14079
14080        let program = Program::parse(initial_source).unwrap().0.unwrap();
14081
14082        let mut frontend = FrontendState::new();
14083
14084        let ctx = ExecutorContext::new_mock(None).await;
14085        let version = Version(0);
14086
14087        frontend.program = program.clone();
14088        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14089        frontend.update_state_after_exec(outcome, true);
14090        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14091        let sketch_id = sketch_object.id;
14092        let sketch = expect_sketch(sketch_object);
14093        let line1_id = *sketch.segments.get(2).unwrap();
14094        let line2_id = *sketch.segments.get(5).unwrap();
14095        let axis_id = *sketch.segments.get(8).unwrap();
14096
14097        let constraint = Constraint::Symmetric(Symmetric {
14098            input: vec![line1_id, line2_id],
14099            axis: axis_id,
14100        });
14101        let (src_delta, scene_delta) = frontend
14102            .add_constraint(&ctx, version, sketch_id, constraint)
14103            .await
14104            .unwrap();
14105        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14106        assert_eq!(
14107            scene_delta.new_graph.objects.len(),
14108            12,
14109            "{:#?}",
14110            scene_delta.new_graph.objects
14111        );
14112
14113        ctx.close().await;
14114    }
14115
14116    #[tokio::test(flavor = "multi_thread")]
14117    async fn test_point_arc_midpoint() {
14118        let initial_source = "\
14119sketch(on = XY) {
14120  point(at = [var 6, var 3])
14121  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14122}
14123";
14124
14125        let program = Program::parse(initial_source).unwrap().0.unwrap();
14126
14127        let mut frontend = FrontendState::new();
14128
14129        let ctx = ExecutorContext::new_mock(None).await;
14130        let version = Version(0);
14131
14132        frontend.program = program.clone();
14133        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14134        frontend.update_state_after_exec(outcome, true);
14135        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14136        let sketch_id = sketch_object.id;
14137        let sketch = expect_sketch(sketch_object);
14138        let point_id = *sketch.segments.first().unwrap();
14139        let arc_id = *sketch.segments.get(4).unwrap();
14140
14141        let constraint = Constraint::Midpoint(Midpoint {
14142            point: ConstraintSegment::from(point_id),
14143            segment: arc_id,
14144        });
14145        let (src_delta, scene_delta) = frontend
14146            .add_constraint(&ctx, version, sketch_id, constraint)
14147            .await
14148            .unwrap();
14149        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14150        assert_eq!(
14151            scene_delta.new_graph.objects.len(),
14152            8,
14153            "{:#?}",
14154            scene_delta.new_graph.objects
14155        );
14156
14157        ctx.close().await;
14158    }
14159
14160    #[tokio::test(flavor = "multi_thread")]
14161    async fn test_origin_line_midpoint() {
14162        let initial_source = "\
14163sketch(on = XY) {
14164  line(start = [var 0, var 0], end = [var 6, var 4])
14165}
14166";
14167
14168        let program = Program::parse(initial_source).unwrap().0.unwrap();
14169
14170        let mut frontend = FrontendState::new();
14171
14172        let ctx = ExecutorContext::new_mock(None).await;
14173        let version = Version(0);
14174
14175        frontend.program = program.clone();
14176        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14177        frontend.update_state_after_exec(outcome, true);
14178        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14179        let sketch_id = sketch_object.id;
14180        let sketch = expect_sketch(sketch_object);
14181        let line_id = *sketch.segments.get(2).unwrap();
14182
14183        let constraint = Constraint::Midpoint(Midpoint {
14184            point: ConstraintSegment::ORIGIN,
14185            segment: line_id,
14186        });
14187        let (src_delta, scene_delta) = frontend
14188            .add_constraint(&ctx, version, sketch_id, constraint)
14189            .await
14190            .unwrap();
14191        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14192        assert_eq!(
14193            scene_delta.new_graph.objects.len(),
14194            6,
14195            "{:#?}",
14196            scene_delta.new_graph.objects
14197        );
14198
14199        ctx.close().await;
14200    }
14201
14202    #[tokio::test(flavor = "multi_thread")]
14203    async fn test_origin_arc_midpoint() {
14204        let initial_source = "\
14205sketch(on = XY) {
14206  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14207}
14208";
14209
14210        let program = Program::parse(initial_source).unwrap().0.unwrap();
14211
14212        let mut frontend = FrontendState::new();
14213
14214        let ctx = ExecutorContext::new_mock(None).await;
14215        let version = Version(0);
14216
14217        frontend.program = program.clone();
14218        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14219        frontend.update_state_after_exec(outcome, true);
14220        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14221        let sketch_id = sketch_object.id;
14222        let sketch = expect_sketch(sketch_object);
14223        let arc_id = *sketch.segments.get(3).unwrap();
14224
14225        let constraint = Constraint::Midpoint(Midpoint {
14226            point: ConstraintSegment::ORIGIN,
14227            segment: arc_id,
14228        });
14229        let (src_delta, scene_delta) = frontend
14230            .add_constraint(&ctx, version, sketch_id, constraint)
14231            .await
14232            .unwrap();
14233        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14234        assert_eq!(
14235            scene_delta.new_graph.objects.len(),
14236            7,
14237            "{:#?}",
14238            scene_delta.new_graph.objects
14239        );
14240
14241        ctx.close().await;
14242    }
14243
14244    #[tokio::test(flavor = "multi_thread")]
14245    async fn test_segments_symmetric_arcs() {
14246        let initial_source = "\
14247sketch(on = XY) {
14248  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14249  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14250  line(start = [var 0, var -10], end = [var 0, var 10])
14251}
14252";
14253
14254        let program = Program::parse(initial_source).unwrap().0.unwrap();
14255
14256        let mut frontend = FrontendState::new();
14257
14258        let ctx = ExecutorContext::new_mock(None).await;
14259        let version = Version(0);
14260
14261        frontend.program = program.clone();
14262        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14263        frontend.update_state_after_exec(outcome, true);
14264        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14265        let sketch_id = sketch_object.id;
14266        let sketch = expect_sketch(sketch_object);
14267        let arc1_id = *sketch.segments.get(3).unwrap();
14268        let arc2_id = *sketch.segments.get(7).unwrap();
14269        let axis_id = *sketch.segments.get(10).unwrap();
14270
14271        let constraint = Constraint::Symmetric(Symmetric {
14272            input: vec![arc1_id, arc2_id],
14273            axis: axis_id,
14274        });
14275        let (src_delta, scene_delta) = frontend
14276            .add_constraint(&ctx, version, sketch_id, constraint)
14277            .await
14278            .unwrap();
14279        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14280        assert_eq!(
14281            scene_delta.new_graph.objects.len(),
14282            14,
14283            "{:#?}",
14284            scene_delta.new_graph.objects
14285        );
14286
14287        ctx.close().await;
14288    }
14289
14290    #[tokio::test(flavor = "multi_thread")]
14291    async fn test_sketch_on_face_simple() {
14292        let initial_source = "\
14293len = 2mm
14294cube = startSketchOn(XY)
14295  |> startProfile(at = [0, 0])
14296  |> line(end = [len, 0], tag = $side)
14297  |> line(end = [0, len])
14298  |> line(end = [-len, 0])
14299  |> line(end = [0, -len])
14300  |> close()
14301  |> extrude(length = len)
14302
14303face = faceOf(cube, face = side)
14304";
14305
14306        let program = Program::parse(initial_source).unwrap().0.unwrap();
14307
14308        let mut frontend = FrontendState::new();
14309
14310        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14311        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14312        let version = Version(0);
14313
14314        frontend.hack_set_program(&ctx, program).await.unwrap();
14315        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14316        let face_id = face_object.id;
14317
14318        let sketch_args = SketchCtor {
14319            on: Plane::Object(face_id),
14320        };
14321        let (_src_delta, scene_delta, sketch_id) = frontend
14322            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14323            .await
14324            .unwrap();
14325        assert_eq!(sketch_id, ObjectId(2));
14326        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14327        let sketch_object = &scene_delta.new_graph.objects[2];
14328        assert_eq!(sketch_object.id, ObjectId(2));
14329        assert_eq!(
14330            sketch_object.kind,
14331            ObjectKind::Sketch(Sketch {
14332                args: SketchCtor {
14333                    on: Plane::Object(face_id),
14334                },
14335                plane: face_id,
14336                segments: vec![],
14337                constraints: vec![],
14338            })
14339        );
14340        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14341
14342        ctx.close().await;
14343        mock_ctx.close().await;
14344    }
14345
14346    #[tokio::test(flavor = "multi_thread")]
14347    async fn test_new_sketch_on_primitive_index_face() {
14348        let initial_source = "\
14349@settings(kclVersion = 2.0)
14350
14351sketch001 = sketch(on = XY) {
14352  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14353}
14354extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14355shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14356        let program = Program::parse(initial_source).unwrap().0.unwrap();
14357        let ctx = ExecutorContext::new_mock(None).await;
14358        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14359        let solid_id = match outcome.variables.get("shell001") {
14360            Some(KclValueView::Solid { value }) => value.id,
14361            value => panic!("expected shell001 to be a solid, got {value:?}"),
14362        };
14363        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14364
14365        let mut ast = program.ast;
14366        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14367        let face_expr = sketch_on_ast_expr(
14368            &mut ast,
14369            &scene_graph,
14370            &solid_references,
14371            &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14372        )
14373        .unwrap();
14374        let face_decl = ast::VariableDeclaration::new(
14375            ast::VariableDeclarator::new("face001", face_expr),
14376            ast::ItemVisibility::Default,
14377            ast::VariableKind::Const,
14378        );
14379        ast.body
14380            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14381                face_decl,
14382            ))));
14383        let face_source = source_from_ast(&ast);
14384        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14385        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14386
14387        let program = Program::parse(&new_source).unwrap().0.unwrap();
14388        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14389        ctx.close().await;
14390    }
14391
14392    #[tokio::test(flavor = "multi_thread")]
14393    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14394        let initial_source = "\
14395s = sketch(on = YZ) {
14396  line1 = line(start = [0, 0], end = [0, 1])
14397  line2 = line(start = [0, 1], end = [1, 1])
14398  line3 = line(start = [1, 1], end = [0, 0])
14399}
14400region001 = region(point = [0.1, 0.1], sketch = s)
14401extrude001 = extrude(region001, length = 5)
14402";
14403
14404        let program = Program::parse(initial_source).unwrap().0.unwrap();
14405
14406        let mut frontend = FrontendState::new();
14407        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14408        let version = Version(0);
14409
14410        frontend.hack_set_program(&ctx, program).await.unwrap();
14411        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14412
14413        let sketch_args = SketchCtor {
14414            on: Plane::Object(wall_object_id),
14415        };
14416        let (src_delta, _scene_delta, _sketch_id) = frontend
14417            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14418            .await
14419            .unwrap();
14420        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14421
14422        ctx.close().await;
14423    }
14424
14425    #[tokio::test(flavor = "multi_thread")]
14426    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14427        let initial_source = "\
14428sketch001 = sketch(on = YZ) {
14429  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14430  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14431  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14432  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14433  coincident([line1.end, line2.start])
14434  coincident([line2.end, line3.start])
14435  coincident([line3.end, line4.start])
14436  coincident([line4.end, line1.start])
14437  parallel([line2, line4])
14438  parallel([line3, line1])
14439  perpendicular([line1, line2])
14440  horizontal(line3)
14441  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14442}
14443region001 = region(point = [3.1, 3.74], sketch = sketch001)
14444extrude001 = extrude(region001, length = 5)
14445";
14446
14447        let program = Program::parse(initial_source).unwrap().0.unwrap();
14448
14449        let mut frontend = FrontendState::new();
14450        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14451        let version = Version(0);
14452
14453        frontend.hack_set_program(&ctx, program).await.unwrap();
14454        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14455
14456        let sketch_args = SketchCtor {
14457            on: Plane::Object(wall_object_id),
14458        };
14459        let (src_delta, _scene_delta, _sketch_id) = frontend
14460            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14461            .await
14462            .unwrap();
14463        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14464
14465        ctx.close().await;
14466    }
14467
14468    #[tokio::test(flavor = "multi_thread")]
14469    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14470        let initial_source = "\
14471@settings(kclVersion = 2.0)
14472
14473sketch001 = sketch(on = XY) {
14474  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14475  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14476}
14477hidden001 = hide(sketch001)
14478region001 = region(segments = [sketch001.circle2])
14479region002 = region(segments = [sketch001.circle1])
14480extrude001 = extrude([region001, region002], length = 5)
14481";
14482
14483        let program = Program::parse(initial_source).unwrap().0.unwrap();
14484        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14485        let version = Version(0);
14486
14487        for (solid_output_index, expected_face) in [
14488            (0, "faceOf(extrude001[0], face = END)"),
14489            (1, "faceOf(extrude001[1], face = END)"),
14490        ] {
14491            let mut frontend = FrontendState::new();
14492            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14493            let cap_object_id = find_cap_object_id_with_solid_output_index(
14494                &frontend.scene_graph,
14495                kcl_api::CapKind::End,
14496                solid_output_index,
14497            )
14498            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14499
14500            let sketch_args = SketchCtor {
14501                on: Plane::Object(cap_object_id),
14502            };
14503            let (src_delta, _scene_delta, _sketch_id) = frontend
14504                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14505                .await
14506                .unwrap();
14507
14508            assert!(
14509                src_delta.text.contains(expected_face),
14510                "expected `{expected_face}` in:\n{}",
14511                src_delta.text
14512            );
14513            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14514        }
14515
14516        ctx.close().await;
14517    }
14518
14519    #[tokio::test(flavor = "multi_thread")]
14520    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14521        let initial_source = "\
14522@settings(kclVersion = 2.0)
14523
14524sketch001 = sketch(on = XY) {
14525  rect1Line1 = line(start = [0, 0], end = [1, 0])
14526  rect1Line2 = line(start = [1, 0], end = [1, 1])
14527  rect1Line3 = line(start = [1, 1], end = [0, 1])
14528  rect1Line4 = line(start = [0, 1], end = [0, 0])
14529  rect2Line1 = line(start = [3, 0], end = [4, 0])
14530  rect2Line2 = line(start = [4, 0], end = [4, 1])
14531  rect2Line3 = line(start = [4, 1], end = [3, 1])
14532  rect2Line4 = line(start = [3, 1], end = [3, 0])
14533}
14534hidden001 = hide(sketch001)
14535region001 = region(segments = [
14536  sketch001.rect1Line4,
14537  sketch001.rect1Line1
14538])
14539region002 = region(segments = [
14540  sketch001.rect2Line4,
14541  sketch001.rect2Line1
14542])
14543extrude001 = extrude([region001, region002], length = 5)
14544";
14545
14546        let program = Program::parse(initial_source).unwrap().0.unwrap();
14547        let mut frontend = FrontendState::new();
14548        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14549        let version = Version(0);
14550
14551        frontend.hack_set_program(&ctx, program).await.unwrap();
14552        let region_call = "\
14553region(segments = [
14554  sketch001.rect1Line4,
14555  sketch001.rect1Line1
14556])";
14557        let region_call_start = initial_source.find(region_call).unwrap();
14558        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14559        let segment_call = "line(start = [0, 0], end = [1, 0])";
14560        let segment_call_start = initial_source.find(segment_call).unwrap();
14561        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14562        let wall_object_id = frontend
14563            .scene_graph
14564            .objects
14565            .iter()
14566            .find_map(|object| match &object.kind {
14567                ObjectKind::Wall(wall)
14568                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14569                        && wall.source.segment.range == segment_range =>
14570                {
14571                    Some(object.id)
14572                }
14573                _ => None,
14574            })
14575            .expect("expected a wall object for region001.tags.rect1Line1");
14576
14577        let sketch_args = SketchCtor {
14578            on: Plane::Object(wall_object_id),
14579        };
14580        let (src_delta, _scene_delta, _sketch_id) = frontend
14581            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14582            .await
14583            .unwrap();
14584
14585        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14586        assert!(
14587            src_delta.text.contains(expected_face),
14588            "expected `{expected_face}` in:\n{}",
14589            src_delta.text
14590        );
14591        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14592
14593        ctx.close().await;
14594    }
14595
14596    #[test]
14597    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14598        let source = "\
14599sketch001 = sketch(on = XY) {
14600  line(start = [0, 0], end = [1, 0])
14601}
14602part = subtract(boxSolid, tools = [cutSolid])
14603  |> appearance(color = \"#8f96a3\")
14604";
14605        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14606        let line_start = source.find("line").unwrap();
14607        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14608        let line_ref = SourceRef::Simple {
14609            range: [line_start, line_end, 0].into(),
14610            node_path: None,
14611        };
14612        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14613
14614        let subtract_start = source.find("subtract").unwrap();
14615        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14616        let subtract_ref = SourceRef::Simple {
14617            range: [subtract_start, subtract_end, 0].into(),
14618            node_path: None,
14619        };
14620        assert_eq!(
14621            variable_name_containing_source_ref(&ast, &subtract_ref),
14622            Some("part".to_owned())
14623        );
14624    }
14625
14626    #[tokio::test(flavor = "multi_thread")]
14627    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14628        let source = "\
14629boxSolid = startSketchOn(XY)
14630  |> startProfile(at = [0, 0])
14631  |> line(end = [4, 0], tag = $bottomEdge)
14632  |> line(end = [0, 4])
14633  |> line(end = [-4, 0])
14634  |> close()
14635  |> extrude(length = 10)
14636cutSolid = startSketchOn(XY)
14637  |> startProfile(at = [1, 1])
14638  |> line(end = [1, 0])
14639  |> line(end = [0, 1])
14640  |> line(end = [-1, 0])
14641  |> close()
14642  |> extrude(length = 10)
14643part = subtract(boxSolid, tools = [cutSolid])
14644  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14645";
14646        let program = Program::parse(source).unwrap().0.unwrap();
14647        let mut frontend = FrontendState::new();
14648        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14649        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14650            SetProgramOutcome::Success { .. } => {}
14651            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14652        }
14653
14654        let sweep_call_start = source.find("extrude").unwrap();
14655        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14656        let part_call_start = source.find("subtract").unwrap();
14657        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14658        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14659        let composite_range = [part_call_start, part_call_end, 0].into();
14660
14661        let cap_object = frontend
14662            .scene_graph
14663            .objects
14664            .iter()
14665            .find(|object| {
14666                matches!(
14667                    &object.kind,
14668                    ObjectKind::Cap(kcl_api::Cap {
14669                        kind: kcl_api::CapKind::End,
14670                        source,
14671                        ..
14672                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14673                )
14674            })
14675            .expect("expected end cap object to trace through subtract and original extrude");
14676
14677        let mut ast = frontend.program.ast.clone();
14678        let cap_expr = sketch_on_ast_expr(
14679            &mut ast,
14680            &frontend.scene_graph,
14681            &frontend.solid_references,
14682            &Plane::Object(cap_object.id),
14683        )
14684        .unwrap();
14685        let cap_face_decl = ast::VariableDeclaration::new(
14686            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14687            ast::ItemVisibility::Default,
14688            ast::VariableKind::Const,
14689        );
14690        ast.body
14691            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14692                cap_face_decl,
14693            ))));
14694        let generated_source = source_from_ast(&ast);
14695
14696        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14697        assert!(!generated_source.contains("faceOf(boxSolid"));
14698        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14699            panic!("expected faceOf call");
14700        };
14701        assert_eq!(call.callee.name.name, "faceOf");
14702        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14703            panic!("expected solid name");
14704        };
14705        assert_eq!(solid_name.name.name, "part");
14706        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14707            panic!("expected face name");
14708        };
14709        assert_eq!(face_name.name.name, "END");
14710
14711        ctx.close().await;
14712    }
14713
14714    #[tokio::test(flavor = "multi_thread")]
14715    async fn test_sketch_on_plane_incremental() {
14716        let initial_source = "\
14717len = 2mm
14718cube = startSketchOn(XY)
14719  |> startProfile(at = [0, 0])
14720  |> line(end = [len, 0], tag = $side)
14721  |> line(end = [0, len])
14722  |> line(end = [-len, 0])
14723  |> line(end = [0, -len])
14724  |> close()
14725  |> extrude(length = len)
14726
14727plane = planeOf(cube, face = side)
14728";
14729
14730        let program = Program::parse(initial_source).unwrap().0.unwrap();
14731
14732        let mut frontend = FrontendState::new();
14733
14734        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14735        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14736        let version = Version(0);
14737
14738        frontend.hack_set_program(&ctx, program).await.unwrap();
14739        // Find the last plane since the first plane is the XY plane.
14740        let plane_object = frontend
14741            .scene_graph
14742            .objects
14743            .iter()
14744            .rev()
14745            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14746            .unwrap();
14747        let plane_id = plane_object.id;
14748
14749        let sketch_args = SketchCtor {
14750            on: Plane::Object(plane_id),
14751        };
14752        let (src_delta, scene_delta, sketch_id) = frontend
14753            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14754            .await
14755            .unwrap();
14756        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14757        assert_eq!(sketch_id, ObjectId(2));
14758        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14759        let sketch_object = &scene_delta.new_graph.objects[2];
14760        assert_eq!(sketch_object.id, ObjectId(2));
14761        assert_eq!(
14762            sketch_object.kind,
14763            ObjectKind::Sketch(Sketch {
14764                args: SketchCtor {
14765                    on: Plane::Object(plane_id),
14766                },
14767                plane: plane_id,
14768                segments: vec![],
14769                constraints: vec![],
14770            })
14771        );
14772        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14773
14774        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14775        assert_eq!(plane_object.id, plane_id);
14776        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14777
14778        ctx.close().await;
14779        mock_ctx.close().await;
14780    }
14781
14782    #[tokio::test(flavor = "multi_thread")]
14783    async fn test_new_sketch_uses_unique_variable_name() {
14784        let initial_source = "\
14785sketch1 = sketch(on = XY) {
14786}
14787";
14788
14789        let program = Program::parse(initial_source).unwrap().0.unwrap();
14790
14791        let mut frontend = FrontendState::new();
14792        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14793        let version = Version(0);
14794
14795        frontend.hack_set_program(&ctx, program).await.unwrap();
14796
14797        let sketch_args = SketchCtor {
14798            on: Plane::Default(PlaneName::Yz),
14799        };
14800        let (src_delta, _, _) = frontend
14801            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14802            .await
14803            .unwrap();
14804
14805        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14806
14807        ctx.close().await;
14808    }
14809
14810    #[tokio::test(flavor = "multi_thread")]
14811    async fn test_new_sketch_twice_using_same_plane() {
14812        let initial_source = "\
14813sketch1 = sketch(on = XY) {
14814}
14815";
14816
14817        let program = Program::parse(initial_source).unwrap().0.unwrap();
14818
14819        let mut frontend = FrontendState::new();
14820        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14821        let version = Version(0);
14822
14823        frontend.hack_set_program(&ctx, program).await.unwrap();
14824
14825        let sketch_args = SketchCtor {
14826            on: Plane::Default(PlaneName::Xy),
14827        };
14828        let (src_delta, _, _) = frontend
14829            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14830            .await
14831            .unwrap();
14832
14833        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14834
14835        ctx.close().await;
14836    }
14837
14838    #[tokio::test(flavor = "multi_thread")]
14839    async fn test_sketch_mode_reuses_cached_on_expression() {
14840        let initial_source = "\
14841width = 2mm
14842sketch(on = offsetPlane(XY, offset = width)) {
14843  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14844  distance([line1.start, line1.end]) == width
14845}
14846";
14847        let program = Program::parse(initial_source).unwrap().0.unwrap();
14848
14849        let mut frontend = FrontendState::new();
14850        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14851        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
14852        let version = Version(0);
14853        let project_id = ProjectId(0);
14854        let file_id = FileId(0);
14855
14856        frontend.hack_set_program(&ctx, program).await.unwrap();
14857        let initial_object_count = frontend.scene_graph.objects.len();
14858        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14859            .expect("Expected sketch object to exist")
14860            .id;
14861
14862        // Entering sketch mode should reuse cached `on` expression state
14863        // (offsetPlane result), not fail or create extra on-surface objects.
14864        let scene_delta = frontend
14865            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14866            .await
14867            .unwrap();
14868        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14869
14870        // A follow-up sketch-mode execution should keep the same stable object
14871        // graph shape as well.
14872        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14873        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14874
14875        ctx.close().await;
14876        mock_ctx.close().await;
14877    }
14878
14879    #[tokio::test(flavor = "multi_thread")]
14880    async fn test_edit_sketch_nested_in_pipe() {
14881        let source = r#"
14882profile = sketch(on = XY) {
14883  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14884}
14885  |> translate(x = 2mm)
14886"#;
14887        let program = Program::parse_no_errs(source).unwrap();
14888        let mut frontend = FrontendState::new();
14889        let mock_ctx = ExecutorContext::new_mock(None).await;
14890        let version = Version(0);
14891
14892        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14893        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14894            .expect("Expected piped sketch object")
14895            .id;
14896
14897        let scene_delta = frontend
14898            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14899            .await
14900            .unwrap();
14901        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14902        assert!(
14903            scene_delta
14904                .new_graph
14905                .objects
14906                .iter()
14907                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14908            "Expected the piped sketch's segments to be present in sketch mode"
14909        );
14910
14911        mock_ctx.cache.clear_mem_cache().await;
14912        mock_ctx.close().await;
14913    }
14914
14915    #[tokio::test(flavor = "multi_thread")]
14916    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14917        let source = r#"
14918useFirstProfile = true
14919
14920profile = if useFirstProfile {
14921  sketch(on = XY) {
14922    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14923  }
14924} else {
14925  sketch(on = XY) {
14926    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14927  }
14928}
14929"#;
14930        let program = Program::parse_no_errs(source).unwrap();
14931        let mut frontend = FrontendState::new();
14932        let mock_ctx = ExecutorContext::new_mock(None).await;
14933        let version = Version(0);
14934
14935        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14936        let sketch_object =
14937            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14938        let sketch_id = sketch_object.id;
14939        let sketch = expect_sketch(sketch_object);
14940        let line_end_id = *sketch
14941            .segments
14942            .get(1)
14943            .expect("Expected the active branch's line end point");
14944
14945        let scene_delta = frontend
14946            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14947            .await
14948            .unwrap();
14949        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14950
14951        let segments = vec![ExistingSegmentCtor {
14952            id: line_end_id,
14953            ctor: SegmentCtor::Point(PointCtor {
14954                position: Point2d {
14955                    x: Expr::Var(Number {
14956                        value: 30.0,
14957                        units: NumericSuffix::Mm,
14958                    }),
14959                    y: Expr::Var(Number {
14960                        value: 15.0,
14961                        units: NumericSuffix::Mm,
14962                    }),
14963                },
14964            }),
14965        }];
14966        let (source_delta, _) = frontend
14967            .edit_segments(&mock_ctx, version, sketch_id, segments)
14968            .await
14969            .unwrap();
14970        assert!(
14971            source_delta
14972                .text
14973                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14974            "Expected the active branch's dragged variables to be updated:\n{}",
14975            source_delta.text
14976        );
14977        assert!(
14978            source_delta
14979                .text
14980                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14981            "Expected the inactive branch to remain unchanged:\n{}",
14982            source_delta.text
14983        );
14984
14985        mock_ctx.cache.clear_mem_cache().await;
14986        mock_ctx.close().await;
14987    }
14988
14989    #[tokio::test(flavor = "multi_thread")]
14990    async fn test_multiple_sketch_blocks() {
14991        let initial_source = "\
14992// Cube that requires the engine.
14993width = 2
14994sketch001 = startSketchOn(XY)
14995profile001 = startProfile(sketch001, at = [0, 0])
14996  |> yLine(length = width, tag = $seg1)
14997  |> xLine(length = width)
14998  |> yLine(length = -width)
14999  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
15000  |> close()
15001extrude001 = extrude(profile001, length = width)
15002
15003// Get a value that requires the engine.
15004x = segLen(seg1)
15005
15006// Triangle with side length 2*x.
15007sketch(on = XY) {
15008  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15009  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15010  coincident([line1.end, line2.start])
15011  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15012  coincident([line2.end, line3.start])
15013  coincident([line3.end, line1.start])
15014  equalLength([line3, line1])
15015  equalLength([line1, line2])
15016  distance([line1.start, line1.end]) == 2*x
15017}
15018
15019// Line segment with length x.
15020sketch2 = sketch(on = XY) {
15021  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15022  distance([line1.start, line1.end]) == x
15023}
15024";
15025
15026        let program = Program::parse(initial_source).unwrap().0.unwrap();
15027
15028        let mut frontend = FrontendState::new();
15029
15030        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15031        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15032        let version = Version(0);
15033        let project_id = ProjectId(0);
15034        let file_id = FileId(0);
15035
15036        frontend.hack_set_program(&ctx, program).await.unwrap();
15037        let sketch_objects = frontend
15038            .scene_graph
15039            .objects
15040            .iter()
15041            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15042            .collect::<Vec<_>>();
15043        let sketch1_id = sketch_objects.first().unwrap().id;
15044        let sketch2_id = sketch_objects.get(1).unwrap().id;
15045        // First point in sketch1.
15046        let point1_id = ObjectId(sketch1_id.0 + 1);
15047        // First point in sketch2.
15048        let point2_id = ObjectId(sketch2_id.0 + 1);
15049
15050        // Edit the first sketch. Objects before the sketch block should be
15051        // present from execution cache so that we can sketch on prior planes,
15052        // for example. Objects after the first sketch block should not be
15053        // present since those statements are skipped in sketch mode.
15054        //
15055        // - startSketchOn(XY) Plane 1
15056        // - sketch on=XY Plane 1
15057        // - Sketch block 16
15058        let scene_delta = frontend
15059            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15060            .await
15061            .unwrap();
15062        assert_eq!(
15063            scene_delta.new_graph.objects.len(),
15064            18,
15065            "{:#?}",
15066            scene_delta.new_graph.objects
15067        );
15068
15069        // Edit a point in the first sketch.
15070        let point_ctor = PointCtor {
15071            position: Point2d {
15072                x: Expr::Var(Number {
15073                    value: 1.0,
15074                    units: NumericSuffix::Mm,
15075                }),
15076                y: Expr::Var(Number {
15077                    value: 2.0,
15078                    units: NumericSuffix::Mm,
15079                }),
15080            },
15081        };
15082        let segments = vec![ExistingSegmentCtor {
15083            id: point1_id,
15084            ctor: SegmentCtor::Point(point_ctor),
15085        }];
15086        let (src_delta, _) = frontend
15087            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15088            .await
15089            .unwrap();
15090        // Only the first sketch block changes.
15091        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15092        let edited_sketch1_source = src_delta.text.clone();
15093
15094        // Execute mock to simulate drag end.
15095        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15096        assert_eq!(src_delta.text, edited_sketch1_source);
15097        // Exit sketch. Objects from the entire program should be present.
15098        //
15099        // - startSketchOn(XY) Plane 1
15100        // - sketch on=XY Plane 1
15101        // - Sketch block 16
15102        // - sketch on=XY cached
15103        // - Sketch block 5
15104        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15105        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15106
15107        // Edit the second sketch.
15108        //
15109        // - startSketchOn(XY) Plane 1
15110        // - sketch on=XY Plane 1
15111        // - Sketch block 16
15112        // - sketch on=XY cached
15113        // - Sketch block 5
15114        let scene_delta = frontend
15115            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15116            .await
15117            .unwrap();
15118        assert_eq!(
15119            scene_delta.new_graph.objects.len(),
15120            24,
15121            "{:#?}",
15122            scene_delta.new_graph.objects
15123        );
15124
15125        // Edit a point in the second sketch.
15126        let point_ctor = PointCtor {
15127            position: Point2d {
15128                x: Expr::Var(Number {
15129                    value: 3.0,
15130                    units: NumericSuffix::Mm,
15131                }),
15132                y: Expr::Var(Number {
15133                    value: 4.0,
15134                    units: NumericSuffix::Mm,
15135                }),
15136            },
15137        };
15138        let segments = vec![ExistingSegmentCtor {
15139            id: point2_id,
15140            ctor: SegmentCtor::Point(point_ctor),
15141        }];
15142        let (src_delta, _) = frontend
15143            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15144            .await
15145            .unwrap();
15146        // Only the second sketch block changes.
15147        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15148        let edited_sketch2_source = src_delta.text.clone();
15149
15150        // Execute mock to simulate drag end.
15151        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15152        assert_eq!(src_delta.text, edited_sketch2_source);
15153
15154        ctx.close().await;
15155        mock_ctx.close().await;
15156    }
15157
15158    #[tokio::test(flavor = "multi_thread")]
15159    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15160        let source = r#"sketch001 = sketch(on = XZ) {
15161  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15162}
15163sketch002 = sketch(on = XY) {
15164  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15165  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15166  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15167  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15168  coincident([line1.end, line2.start])
15169  coincident([line2.end, line3.start])
15170  coincident([line3.end, line4.start])
15171  coincident([line4.end, line1.start])
15172  parallel([line2, line4])
15173  parallel([line3, line1])
15174  perpendicular([line1, line2])
15175  horizontal(line3)
15176  coincident([line1.start, ORIGIN])
15177}
15178"#;
15179
15180        let program = Program::parse(source).unwrap().0.unwrap();
15181        let mut frontend = FrontendState::new();
15182        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15183        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15184        let version = Version(0);
15185        let project_id = ProjectId(0);
15186        let file_id = FileId(0);
15187
15188        frontend.hack_set_program(&ctx, program).await.unwrap();
15189        let sketch_objects = frontend
15190            .scene_graph
15191            .objects
15192            .iter()
15193            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15194            .collect::<Vec<_>>();
15195        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15196
15197        let sketch1_id = sketch_objects[0].id;
15198        let sketch2_id = sketch_objects[1].id;
15199
15200        frontend
15201            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15202            .await
15203            .unwrap();
15204        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15205
15206        let scene_delta = frontend
15207            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15208            .await
15209            .unwrap();
15210        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15211
15212        ctx.cache.clear_mem_cache().await;
15213        ctx.close().await;
15214        mock_ctx.close().await;
15215    }
15216
15217    // Regression tests: operations on source code with extra whitespace/newlines.
15218    // These test that NodePath-based lookups work correctly when source ranges
15219    // are shifted by extra whitespace that wouldn't be present after formatting.
15220
15221    #[tokio::test(flavor = "multi_thread")]
15222    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15223        // Extra newlines after @settings line - this shifts all source ranges.
15224        let initial_source = "@settings(defaultLengthUnit = mm)
15225
15226sketch001 = sketch(on = XY) {
15227  point(at = [1in, 2in])
15228}
15229";
15230
15231        let program = Program::parse(initial_source).unwrap().0.unwrap();
15232        let mut frontend = FrontendState::new();
15233
15234        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15235        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15236        let version = Version(0);
15237        let project_id = ProjectId(0);
15238        let file_id = FileId(0);
15239
15240        frontend.hack_set_program(&ctx, program).await.unwrap();
15241        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15242        let sketch_id = sketch_object.id;
15243
15244        // Edit sketch should succeed despite extra newlines.
15245        frontend
15246            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15247            .await
15248            .unwrap();
15249
15250        // Add a new point to the sketch.
15251        let point_ctor = PointCtor {
15252            position: Point2d {
15253                x: Expr::Number(Number {
15254                    value: 5.0,
15255                    units: NumericSuffix::Mm,
15256                }),
15257                y: Expr::Number(Number {
15258                    value: 6.0,
15259                    units: NumericSuffix::Mm,
15260                }),
15261            },
15262        };
15263        let segment = SegmentCtor::Point(point_ctor);
15264        let (src_delta, scene_delta) = frontend
15265            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15266            .await
15267            .unwrap();
15268        // After adding a point, the source should be reformatted with standard whitespace.
15269        assert!(
15270            src_delta.text.contains("point(at = [5mm, 6mm])"),
15271            "Expected new point in source, got: {}",
15272            src_delta.text
15273        );
15274        assert!(!scene_delta.new_objects.is_empty());
15275
15276        ctx.close().await;
15277        mock_ctx.close().await;
15278    }
15279
15280    #[tokio::test(flavor = "multi_thread")]
15281    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15282        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15283
15284        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15285        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15286
15287        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15288        assert!(
15289            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15290            "Expected experimental settings to be added to source"
15291        );
15292    }
15293
15294    #[tokio::test(flavor = "multi_thread")]
15295    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15296        // Extra newlines after @settings, with an empty sketch block.
15297        let initial_source = "@settings(defaultLengthUnit = mm)
15298
15299s = sketch(on = XY) {}
15300";
15301
15302        let program = Program::parse(initial_source).unwrap().0.unwrap();
15303        let mut frontend = FrontendState::new();
15304
15305        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15306        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15307        let version = Version(0);
15308
15309        frontend.hack_set_program(&ctx, program).await.unwrap();
15310        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15311        let sketch_id = sketch_object.id;
15312
15313        let line_ctor = LineCtor {
15314            start: Point2d {
15315                x: Expr::Number(Number {
15316                    value: 0.0,
15317                    units: NumericSuffix::Mm,
15318                }),
15319                y: Expr::Number(Number {
15320                    value: 0.0,
15321                    units: NumericSuffix::Mm,
15322                }),
15323            },
15324            end: Point2d {
15325                x: Expr::Number(Number {
15326                    value: 10.0,
15327                    units: NumericSuffix::Mm,
15328                }),
15329                y: Expr::Number(Number {
15330                    value: 10.0,
15331                    units: NumericSuffix::Mm,
15332                }),
15333            },
15334            construction: None,
15335        };
15336        let segment = SegmentCtor::Line(line_ctor);
15337        let (src_delta, scene_delta) = frontend
15338            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15339            .await
15340            .unwrap();
15341        assert!(
15342            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15343            "Expected line in source, got: {}",
15344            src_delta.text
15345        );
15346        // Line creates start point, end point, and line segment.
15347        assert_eq!(scene_delta.new_objects.len(), 3);
15348
15349        ctx.close().await;
15350        mock_ctx.close().await;
15351    }
15352
15353    #[tokio::test(flavor = "multi_thread")]
15354    async fn test_extra_newlines_between_operations_edit_line() {
15355        // Extra newlines between @settings and sketch, and inside the sketch block.
15356        let initial_source = "@settings(defaultLengthUnit = mm)
15357
15358sketch001 = sketch(on = XY) {
15359
15360  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15361
15362}
15363";
15364
15365        let program = Program::parse(initial_source).unwrap().0.unwrap();
15366        let mut frontend = FrontendState::new();
15367
15368        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15369        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15370        let version = Version(0);
15371        let project_id = ProjectId(0);
15372        let file_id = FileId(0);
15373
15374        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15375        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15376        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15377        let sketch_id = sketch_object.id;
15378        let sketch = expect_sketch(sketch_object);
15379
15380        // Extract segment IDs before edit_sketch borrows frontend mutably.
15381        let line_id = sketch
15382            .segments
15383            .iter()
15384            .copied()
15385            .find(|seg_id| {
15386                matches!(
15387                    &frontend.scene_graph.objects[seg_id.0].kind,
15388                    ObjectKind::Segment {
15389                        segment: Segment::Line(_)
15390                    }
15391                )
15392            })
15393            .expect("Expected a line segment in sketch");
15394
15395        // Enter sketch edit mode.
15396        frontend
15397            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15398            .await
15399            .unwrap();
15400
15401        // Edit the line.
15402        let line_ctor = LineCtor {
15403            start: Point2d {
15404                x: Expr::Var(Number {
15405                    value: 1.0,
15406                    units: NumericSuffix::Mm,
15407                }),
15408                y: Expr::Var(Number {
15409                    value: 2.0,
15410                    units: NumericSuffix::Mm,
15411                }),
15412            },
15413            end: Point2d {
15414                x: Expr::Var(Number {
15415                    value: 13.0,
15416                    units: NumericSuffix::Mm,
15417                }),
15418                y: Expr::Var(Number {
15419                    value: 14.0,
15420                    units: NumericSuffix::Mm,
15421                }),
15422            },
15423            construction: None,
15424        };
15425        let segments = vec![ExistingSegmentCtor {
15426            id: line_id,
15427            ctor: SegmentCtor::Line(line_ctor),
15428        }];
15429        let (src_delta, _scene_delta) = frontend
15430            .edit_segments(&mock_ctx, version, sketch_id, segments)
15431            .await
15432            .unwrap();
15433        assert!(
15434            src_delta
15435                .text
15436                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15437            "Expected edited line in source, got: {}",
15438            src_delta.text
15439        );
15440
15441        ctx.close().await;
15442        mock_ctx.close().await;
15443    }
15444
15445    #[tokio::test(flavor = "multi_thread")]
15446    async fn test_extra_newlines_delete_segment() {
15447        // Extra whitespace before and after the sketch block.
15448        let initial_source = "@settings(defaultLengthUnit = mm)
15449
15450sketch001 = sketch(on = XY) {
15451  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15452}
15453";
15454
15455        let program = Program::parse(initial_source).unwrap().0.unwrap();
15456        let mut frontend = FrontendState::new();
15457
15458        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15459        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15460        let version = Version(0);
15461
15462        frontend.hack_set_program(&ctx, program).await.unwrap();
15463        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15464        let sketch_id = sketch_object.id;
15465        let sketch = expect_sketch(sketch_object);
15466
15467        // The sketch should have 3 segments: start point, center point, and the circle.
15468        assert_eq!(sketch.segments.len(), 3);
15469        let circle_id = sketch.segments[2];
15470
15471        // Delete the circle despite extra newlines in original source.
15472        let (src_delta, scene_delta) = frontend
15473            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15474            .await
15475            .unwrap();
15476        assert!(
15477            src_delta.text.contains("sketch(on = XY) {"),
15478            "Expected sketch block in source, got: {}",
15479            src_delta.text
15480        );
15481        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15482        let new_sketch = expect_sketch(new_sketch_object);
15483        assert_eq!(new_sketch.segments.len(), 0);
15484
15485        ctx.close().await;
15486        mock_ctx.close().await;
15487    }
15488
15489    #[tokio::test(flavor = "multi_thread")]
15490    async fn test_unformatted_source_add_arc() {
15491        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15492        let initial_source = "@settings(defaultLengthUnit = mm)
15493
15494sketch001 = sketch(on = XY) {
15495}
15496";
15497
15498        let program = Program::parse(initial_source).unwrap().0.unwrap();
15499        let mut frontend = FrontendState::new();
15500
15501        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15502        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15503        let version = Version(0);
15504
15505        frontend.hack_set_program(&ctx, program).await.unwrap();
15506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15507        let sketch_id = sketch_object.id;
15508
15509        let arc_ctor = ArcCtor {
15510            start: Point2d {
15511                x: Expr::Var(Number {
15512                    value: 5.0,
15513                    units: NumericSuffix::Mm,
15514                }),
15515                y: Expr::Var(Number {
15516                    value: 0.0,
15517                    units: NumericSuffix::Mm,
15518                }),
15519            },
15520            end: Point2d {
15521                x: Expr::Var(Number {
15522                    value: 0.0,
15523                    units: NumericSuffix::Mm,
15524                }),
15525                y: Expr::Var(Number {
15526                    value: 5.0,
15527                    units: NumericSuffix::Mm,
15528                }),
15529            },
15530            center: Point2d {
15531                x: Expr::Var(Number {
15532                    value: 0.0,
15533                    units: NumericSuffix::Mm,
15534                }),
15535                y: Expr::Var(Number {
15536                    value: 0.0,
15537                    units: NumericSuffix::Mm,
15538                }),
15539            },
15540            direction: None,
15541            construction: None,
15542        };
15543        let segment = SegmentCtor::Arc(arc_ctor);
15544        let (src_delta, scene_delta) = frontend
15545            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15546            .await
15547            .unwrap();
15548        assert!(
15549            src_delta
15550                .text
15551                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15552            "Expected arc in source, got: {}",
15553            src_delta.text
15554        );
15555        assert!(!scene_delta.new_objects.is_empty());
15556
15557        ctx.close().await;
15558        mock_ctx.close().await;
15559    }
15560
15561    #[tokio::test(flavor = "multi_thread")]
15562    async fn test_arc_direction_flows_to_source() {
15563        let initial_source = "@settings(defaultLengthUnit = mm)
15564
15565sketch001 = sketch(on = XY) {
15566}
15567";
15568
15569        let program = Program::parse(initial_source).unwrap().0.unwrap();
15570        let mut frontend = FrontendState::new();
15571
15572        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15573        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15574        let version = Version(0);
15575
15576        frontend.hack_set_program(&ctx, program).await.unwrap();
15577        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15578        let sketch_id = sketch_object.id;
15579
15580        let point = |x: f64, y: f64| Point2d {
15581            x: Expr::Var(Number {
15582                value: x,
15583                units: NumericSuffix::Mm,
15584            }),
15585            y: Expr::Var(Number {
15586                value: y,
15587                units: NumericSuffix::Mm,
15588            }),
15589        };
15590
15591        // Adding a clockwise arc writes its direction to the source.
15592        let arc_ctor = ArcCtor {
15593            start: point(5.0, 0.0),
15594            end: point(0.0, 5.0),
15595            center: point(0.0, 0.0),
15596            direction: Some(ArcDirection::Cw),
15597            construction: None,
15598        };
15599        let (src_delta, scene_delta) = frontend
15600            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15601            .await
15602            .unwrap();
15603        assert!(
15604            src_delta.text.contains("direction = CW"),
15605            "Expected direction = CW in source, got: {}",
15606            src_delta.text
15607        );
15608        // The new objects are the end points, the center, and then the arc.
15609        let arc_id = *scene_delta.new_objects.last().unwrap();
15610
15611        // Editing the arc's points preserves the clockwise direction. The
15612        // edited points keep the same distance to the center so that the
15613        // solver doesn't need to move anything.
15614        let edited_ctor = ArcCtor {
15615            start: point(0.0, -5.0),
15616            end: point(0.0, 5.0),
15617            center: point(0.0, 0.0),
15618            direction: Some(ArcDirection::Cw),
15619            construction: None,
15620        };
15621        let (src_delta, _scene_delta) = frontend
15622            .edit_segments(
15623                &mock_ctx,
15624                version,
15625                sketch_id,
15626                vec![ExistingSegmentCtor {
15627                    id: arc_id,
15628                    ctor: SegmentCtor::Arc(edited_ctor),
15629                }],
15630            )
15631            .await
15632            .unwrap();
15633        assert!(
15634            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15635            "Expected edited start point in source, got: {}",
15636            src_delta.text
15637        );
15638        assert!(
15639            src_delta.text.contains("direction = CW"),
15640            "Expected direction = CW to be preserved in source, got: {}",
15641            src_delta.text
15642        );
15643
15644        // Editing the arc back to counterclockwise removes the direction
15645        // argument since counterclockwise is the default.
15646        let edited_ctor = ArcCtor {
15647            start: point(0.0, -5.0),
15648            end: point(0.0, 5.0),
15649            center: point(0.0, 0.0),
15650            direction: Some(ArcDirection::Ccw),
15651            construction: None,
15652        };
15653        let (src_delta, _scene_delta) = frontend
15654            .edit_segments(
15655                &mock_ctx,
15656                version,
15657                sketch_id,
15658                vec![ExistingSegmentCtor {
15659                    id: arc_id,
15660                    ctor: SegmentCtor::Arc(edited_ctor),
15661                }],
15662            )
15663            .await
15664            .unwrap();
15665        assert!(
15666            !src_delta.text.contains("direction"),
15667            "Expected direction argument to be removed from source, got: {}",
15668            src_delta.text
15669        );
15670
15671        ctx.close().await;
15672        mock_ctx.close().await;
15673    }
15674
15675    #[tokio::test(flavor = "multi_thread")]
15676    async fn test_extra_newlines_add_circle() {
15677        // Extra blank lines between settings and sketch.
15678        let initial_source = "@settings(defaultLengthUnit = mm)
15679
15680sketch001 = sketch(on = XY) {
15681}
15682";
15683
15684        let program = Program::parse(initial_source).unwrap().0.unwrap();
15685        let mut frontend = FrontendState::new();
15686
15687        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15688        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15689        let version = Version(0);
15690
15691        frontend.hack_set_program(&ctx, program).await.unwrap();
15692        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15693        let sketch_id = sketch_object.id;
15694
15695        let circle_ctor = CircleCtor {
15696            start: Point2d {
15697                x: Expr::Var(Number {
15698                    value: 5.0,
15699                    units: NumericSuffix::Mm,
15700                }),
15701                y: Expr::Var(Number {
15702                    value: 0.0,
15703                    units: NumericSuffix::Mm,
15704                }),
15705            },
15706            center: Point2d {
15707                x: Expr::Var(Number {
15708                    value: 0.0,
15709                    units: NumericSuffix::Mm,
15710                }),
15711                y: Expr::Var(Number {
15712                    value: 0.0,
15713                    units: NumericSuffix::Mm,
15714                }),
15715            },
15716            construction: None,
15717        };
15718        let segment = SegmentCtor::Circle(circle_ctor);
15719        let (src_delta, scene_delta) = frontend
15720            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15721            .await
15722            .unwrap();
15723        assert!(
15724            src_delta
15725                .text
15726                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15727            "Expected circle in source, got: {}",
15728            src_delta.text
15729        );
15730        assert!(!scene_delta.new_objects.is_empty());
15731
15732        ctx.close().await;
15733        mock_ctx.close().await;
15734    }
15735
15736    #[tokio::test(flavor = "multi_thread")]
15737    async fn test_extra_newlines_add_constraint() {
15738        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15739        let initial_source = "@settings(defaultLengthUnit = mm)
15740
15741sketch001 = sketch(on = XY) {
15742  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15743  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15744}
15745";
15746
15747        let program = Program::parse(initial_source).unwrap().0.unwrap();
15748        let mut frontend = FrontendState::new();
15749
15750        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15751        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15752        let version = Version(0);
15753        let project_id = ProjectId(0);
15754        let file_id = FileId(0);
15755
15756        frontend.hack_set_program(&ctx, program).await.unwrap();
15757        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15758        let sketch_id = sketch_object.id;
15759        let sketch = expect_sketch(sketch_object);
15760
15761        // Extract segment data before edit_sketch borrows frontend mutably.
15762        let line_ids: Vec<ObjectId> = sketch
15763            .segments
15764            .iter()
15765            .copied()
15766            .filter(|seg_id| {
15767                matches!(
15768                    &frontend.scene_graph.objects[seg_id.0].kind,
15769                    ObjectKind::Segment {
15770                        segment: Segment::Line(_)
15771                    }
15772                )
15773            })
15774            .collect();
15775        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15776
15777        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15778        let ObjectKind::Segment {
15779            segment: Segment::Line(line1_data),
15780        } = &line1.kind
15781        else {
15782            panic!("Expected line");
15783        };
15784        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15785        let ObjectKind::Segment {
15786            segment: Segment::Line(line2_data),
15787        } = &line2.kind
15788        else {
15789            panic!("Expected line");
15790        };
15791
15792        // Build constraint before entering sketch mode.
15793        let constraint = Constraint::Coincident(Coincident {
15794            segments: vec![line1_data.end.into(), line2_data.start.into()],
15795        });
15796
15797        // Enter sketch edit mode.
15798        frontend
15799            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15800            .await
15801            .unwrap();
15802        let (src_delta, _scene_delta) = frontend
15803            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15804            .await
15805            .unwrap();
15806        assert!(
15807            src_delta.text.contains("coincident("),
15808            "Expected coincident constraint in source, got: {}",
15809            src_delta.text
15810        );
15811
15812        ctx.close().await;
15813        mock_ctx.close().await;
15814    }
15815
15816    #[tokio::test(flavor = "multi_thread")]
15817    async fn test_extra_newlines_add_line_then_edit_line() {
15818        // Extra newlines after @settings - add a line, then edit it.
15819        let initial_source = "@settings(defaultLengthUnit = mm)
15820
15821sketch001 = sketch(on = XY) {
15822}
15823";
15824
15825        let program = Program::parse(initial_source).unwrap().0.unwrap();
15826        let mut frontend = FrontendState::new();
15827
15828        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15829        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
15830        let version = Version(0);
15831
15832        frontend.hack_set_program(&ctx, program).await.unwrap();
15833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15834        let sketch_id = sketch_object.id;
15835
15836        // Add a line.
15837        let line_ctor = LineCtor {
15838            start: Point2d {
15839                x: Expr::Number(Number {
15840                    value: 0.0,
15841                    units: NumericSuffix::Mm,
15842                }),
15843                y: Expr::Number(Number {
15844                    value: 0.0,
15845                    units: NumericSuffix::Mm,
15846                }),
15847            },
15848            end: Point2d {
15849                x: Expr::Number(Number {
15850                    value: 10.0,
15851                    units: NumericSuffix::Mm,
15852                }),
15853                y: Expr::Number(Number {
15854                    value: 10.0,
15855                    units: NumericSuffix::Mm,
15856                }),
15857            },
15858            construction: None,
15859        };
15860        let segment = SegmentCtor::Line(line_ctor);
15861        let (src_delta, scene_delta) = frontend
15862            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15863            .await
15864            .unwrap();
15865        assert!(
15866            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15867            "Expected line in source after add, got: {}",
15868            src_delta.text
15869        );
15870        // Line creates start point, end point, and line segment.
15871        let line_id = *scene_delta.new_objects.last().unwrap();
15872
15873        // Edit the line.
15874        let line_ctor = LineCtor {
15875            start: Point2d {
15876                x: Expr::Number(Number {
15877                    value: 1.0,
15878                    units: NumericSuffix::Mm,
15879                }),
15880                y: Expr::Number(Number {
15881                    value: 2.0,
15882                    units: NumericSuffix::Mm,
15883                }),
15884            },
15885            end: Point2d {
15886                x: Expr::Number(Number {
15887                    value: 13.0,
15888                    units: NumericSuffix::Mm,
15889                }),
15890                y: Expr::Number(Number {
15891                    value: 14.0,
15892                    units: NumericSuffix::Mm,
15893                }),
15894            },
15895            construction: None,
15896        };
15897        let segments = vec![ExistingSegmentCtor {
15898            id: line_id,
15899            ctor: SegmentCtor::Line(line_ctor),
15900        }];
15901        let (src_delta, scene_delta) = frontend
15902            .edit_segments(&mock_ctx, version, sketch_id, segments)
15903            .await
15904            .unwrap();
15905        assert!(
15906            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15907            "Expected edited line in source, got: {}",
15908            src_delta.text
15909        );
15910        assert_eq!(scene_delta.new_objects, vec![]);
15911
15912        ctx.close().await;
15913        mock_ctx.close().await;
15914    }
15915
15916    #[test]
15917    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15918        // A non-target sketch block appears before the target so that the
15919        // traversal enters and leaves it before reaching the target. The
15920        // generated name must come from the top-level scope, where foo1 is
15921        // taken, not the sketch's scope, where no foo names are taken. This
15922        // is a regression test: dfs_mut used to visit the sketch block twice,
15923        // pushing its scope twice but popping it once, leaving the sketch
15924        // scope on top of the defined-names stack for the rest of the
15925        // traversal.
15926        let code = "\
15927foo1 = 1
15928sk = sketch() {
15929  p = var 1.5
15930}
159317 + 8
15932";
15933        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15934        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15935            panic!("expected an expression statement");
15936        };
15937        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15938        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15939            &mut ast,
15940            &source_ref,
15941            AstMutateCommand::AddVariableDeclaration {
15942                prefix: "foo".to_owned(),
15943            },
15944        )
15945        .unwrap();
15946        let AstMutateCommandReturn::Name(name) = cmd_return else {
15947            panic!("expected a generated name");
15948        };
15949        assert_eq!(name, "foo2");
15950        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15951            panic!("expected the expression statement to become a variable declaration");
15952        };
15953        assert_eq!(decl.name(), "foo2");
15954    }
15955
15956    /// Get the function body of the variable declaration at `ast.body[index]`.
15957    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15958        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15959            panic!("expected a variable declaration");
15960        };
15961        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15962            panic!("expected a function expression");
15963        };
15964        &func.body
15965    }
15966
15967    /// Get the then-branch block of the if-expression initializing the
15968    /// variable declaration at `ast.body[index]`.
15969    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15970        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15971            panic!("expected a variable declaration");
15972        };
15973        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15974            panic!("expected an if expression");
15975        };
15976        &if_expr.then_val
15977    }
15978
15979    #[test]
15980    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15981        // The generated name must come from the function body's scope, where
15982        // thing1 is taken. Before dfs_mut visited function bodies as program
15983        // nodes, the top-level scope was used instead, generating thing1 and
15984        // colliding with the local.
15985        let code = "\
15986fn build() {
15987  thing1 = 1
15988  10 + 20
15989  return thing1
15990}
15991";
15992        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15993        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15994            panic!("expected an expression statement");
15995        };
15996        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15997        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15998            &mut ast,
15999            &source_ref,
16000            AstMutateCommand::AddVariableDeclaration {
16001                prefix: "thing".to_owned(),
16002            },
16003        )
16004        .unwrap();
16005        let AstMutateCommandReturn::Name(name) = cmd_return else {
16006            panic!("expected a generated name");
16007        };
16008        assert_eq!(name, "thing2");
16009        let body = &function_body_at(&ast, 0).body;
16010        assert_eq!(body.len(), 3);
16011        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16012            panic!("expected the expression statement to become a variable declaration");
16013        };
16014        assert_eq!(decl.name(), "thing2");
16015        // Siblings are untouched.
16016        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16017            panic!("expected a variable declaration");
16018        };
16019        assert_eq!(first.name(), "thing1");
16020        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16021    }
16022
16023    #[test]
16024    fn test_delete_node_in_function_body_preserves_leading_comment() {
16025        // Before the shared body traversal, MutateBodyItem::Delete was
16026        // silently dropped inside function bodies, so this reported success
16027        // without deleting anything.
16028        let code = "\
16029fn build() {
16030  a = 1
16031  // keep me
16032  b = 2
16033  return a
16034}
16035";
16036        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16037        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16038            panic!("expected a variable declaration");
16039        };
16040        assert_eq!(b_decl.name(), "b");
16041        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16042        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16043        let body = &function_body_at(&ast, 0).body;
16044        assert_eq!(body.len(), 2, "expected b to be deleted");
16045        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16046            panic!("expected a variable declaration");
16047        };
16048        assert_eq!(first.name(), "a");
16049        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16050            panic!("expected the return statement to remain");
16051        };
16052        assert!(
16053            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16054            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16055            body[1].get_comments()
16056        );
16057    }
16058
16059    #[test]
16060    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16061        // Locals in the function body are avoided: thing1 is taken, so the
16062        // generated name is thing2. But find_defined_names only sees the
16063        // block's body items, not the function's parameters, so the generated
16064        // name collides with the thing2 parameter. This pins the current
16065        // behavior.
16066        // TODO: Should function parameters be included in the scope used for
16067        // name generation?
16068        let code = "\
16069fn build(thing2) {
16070  thing1 = 1
16071  10 + 20
16072  return thing1 + thing2
16073}
16074";
16075        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16076        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16077            panic!("expected an expression statement");
16078        };
16079        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16080        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16081            &mut ast,
16082            &source_ref,
16083            AstMutateCommand::AddVariableDeclaration {
16084                prefix: "thing".to_owned(),
16085            },
16086        )
16087        .unwrap();
16088        let AstMutateCommandReturn::Name(name) = cmd_return else {
16089            panic!("expected a generated name");
16090        };
16091        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16092    }
16093
16094    #[test]
16095    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16096        let code = "\
16097x = 1
16098y = if x > 0 {
16099  q1 = 1
16100  foo(q1)
16101  q1
16102} else {
16103  2
16104}
16105";
16106        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16107        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16108            panic!("expected an expression statement");
16109        };
16110        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16111        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16112            &mut ast,
16113            &source_ref,
16114            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16115        )
16116        .unwrap();
16117        let AstMutateCommandReturn::Name(name) = cmd_return else {
16118            panic!("expected a generated name");
16119        };
16120        assert_eq!(name, "q2");
16121        let body = &then_block_at(&ast, 1).body;
16122        assert_eq!(body.len(), 3);
16123        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16124            panic!("expected the expression statement to become a variable declaration");
16125        };
16126        assert_eq!(decl.name(), "q2");
16127        // Siblings are untouched.
16128        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16129            panic!("expected a variable declaration");
16130        };
16131        assert_eq!(first.name(), "q1");
16132        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16133    }
16134
16135    #[test]
16136    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16137        // Before the shared body traversal, MutateBodyItem::Delete was
16138        // silently dropped inside if-expression branch blocks.
16139        let code = "\
16140y = if true {
16141  a = 1
16142  // keep me
16143  b = 2
16144  a + b
16145} else {
16146  2
16147}
16148";
16149        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16150        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16151            panic!("expected a variable declaration");
16152        };
16153        assert_eq!(b_decl.name(), "b");
16154        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16155        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16156        let body = &then_block_at(&ast, 0).body;
16157        assert_eq!(body.len(), 2, "expected b to be deleted");
16158        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16159            panic!("expected a variable declaration");
16160        };
16161        assert_eq!(first.name(), "a");
16162        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16163            panic!("expected the tail expression to remain");
16164        };
16165        assert!(
16166            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16167            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16168            body[1].get_comments()
16169        );
16170    }
16171}