Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112    id: SketchCheckpointId,
113    source: SourceDelta,
114    program: Program,
115    scene_graph: SceneGraph,
116    exec_outcome: ExecOutcome,
117    point_freedom_cache: HashMap<ObjectId, Freedom>,
118    mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123    points: Vec<ObjectId>,
124    function_name: &'static str,
125    value: f64,
126    units: NumericSuffix,
127    label_position: Option<Point2d<Number>>,
128    constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143/// The name of the KCL std constant for a clockwise arc direction.
144const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190    Edit,
191    DeleteNonSketch,
192}
193
194/// Options that control how an edit is re-executed and written back.
195struct ExecuteAfterEditOptions {
196    segment_ids_edited: AhashIndexSet<ObjectId>,
197    edit_kind: EditDeleteKind,
198    commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202    /// Returns true if this edit is any type of deletion.
203    fn is_delete(&self) -> bool {
204        match self {
205            EditDeleteKind::Edit => false,
206            EditDeleteKind::DeleteNonSketch => true,
207        }
208    }
209
210    fn to_change_kind(self) -> ChangeKind {
211        match self {
212            EditDeleteKind::Edit => ChangeKind::Edit,
213            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220    Add,
221    Edit,
222    Delete,
223    None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230    #[serde(rename_all = "camelCase")]
231    Success {
232        scene_graph: Box<SceneGraph>,
233        exec_outcome: Box<ExecOutcome>,
234        checkpoint_id: Option<SketchCheckpointId>,
235    },
236    #[serde(rename_all = "camelCase")]
237    ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240/// Options for a sketch segment edit that participates in drag solving.
241pub struct EditSegmentsOptions {
242    /// Narrows which edited scene objects receive temporary fixed constraints.
243    ///
244    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
245    /// disables those fixed constraints, which is useful for semantic edits such
246    /// as toggling construction state.
247    pub anchor_segment_ids: Option<Vec<ObjectId>>,
248    /// Hidden fixed cursor points that the referenced segment bodies must pass
249    /// through during solve.
250    pub drag_anchors: Vec<SegmentDragAnchor>,
251    /// Constraint label positions to write in the same AST transaction as the
252    /// segment edits. Label positions do not participate in solving.
253    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254    /// Whether solver-updated initial guesses should be written back to KCL.
255    pub commit_solved_initial_guesses: bool,
256}
257
258/// Options for a distance-constraint label edit during sketch dragging.
259pub struct EditDistanceConstraintLabelPositionOptions {
260    /// Edited scene objects to keep anchored while previewing the label edit.
261    pub anchor_segment_ids: Vec<ObjectId>,
262    /// Whether solver-updated initial guesses should be written back to KCL.
263    pub commit_solved_initial_guesses: bool,
264}
265
266/// Options for editing a constraint during sketch dragging.
267pub struct EditConstraintOptions {
268    /// Whether solver-updated initial guesses should be written back to KCL.
269    pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274    variable_name: String,
275    output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280    program: Program,
281    scene_graph: SceneGraph,
282    /// Lightweight map from engine solid IDs to their latest KCL references.
283    solid_references: HashMap<Uuid, SolidAstReference>,
284    /// Stores the last known freedom value for each point object.
285    /// This allows us to preserve freedom values when freedom analysis isn't run.
286    point_freedom_cache: HashMap<ObjectId, Freedom>,
287    /// One-shot drag anchors for the next segment edit. These ids define which
288    /// edited points/segments become temporary fixed constraints during solve.
289    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290    /// One-shot segment-body drag anchors for the next segment edit. These add
291    /// a temporary solver point on the dragged segment that follows the cursor.
292    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293    /// One-shot constraint label edits to apply with the next segment edit.
294    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295    /// One-shot override for whether the next edit commits solver-updated
296    /// initial guesses back into KCL. Drag previews keep this off so only the
297    /// explicit drag edit feeds the next solve.
298    next_edit_commits_solver_solutions: Option<bool>,
299    sketch_checkpoints: VecDeque<SketchCheckpoint>,
300    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl FrontendState {
310    pub fn new() -> Self {
311        Self {
312            program: Program::empty(),
313            scene_graph: SceneGraph {
314                project: ProjectId(0),
315                file: FileId(0),
316                version: Version(0),
317                objects: Default::default(),
318                settings: Default::default(),
319                sketch_mode: Default::default(),
320            },
321            solid_references: HashMap::new(),
322            point_freedom_cache: HashMap::new(),
323            next_drag_anchor_segment_ids: None,
324            next_segment_drag_anchors: None,
325            next_constraint_label_edits: None,
326            next_edit_commits_solver_solutions: None,
327            sketch_checkpoints: VecDeque::new(),
328            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329        }
330    }
331
332    /// Get a reference to the scene graph
333    pub fn scene_graph(&self) -> &SceneGraph {
334        &self.scene_graph
335    }
336
337    pub fn default_length_unit(&self) -> UnitLength {
338        self.program
339            .meta_settings()
340            .ok()
341            .flatten()
342            .map(|settings| settings.default_length_units)
343            .unwrap_or(UnitLength::Millimeters)
344    }
345
346    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349        let checkpoint = SketchCheckpoint {
350            id: checkpoint_id,
351            source: SourceDelta {
352                text: source_from_ast(&self.program.ast),
353            },
354            program: self.program.clone(),
355            scene_graph: self.scene_graph.clone(),
356            exec_outcome,
357            point_freedom_cache: self.point_freedom_cache.clone(),
358            mock_memory: read_old_memory().await,
359        };
360
361        self.sketch_checkpoints.push_back(checkpoint);
362        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363            self.sketch_checkpoints.pop_front();
364        }
365
366        Ok(checkpoint_id)
367    }
368
369    /// Edit sketch segments with optional drag-solve overrides.
370    ///
371    /// Drag anchors add hidden fixed cursor points and constrain the referenced
372    /// segment bodies to pass through them, which lets body drags use the same
373    /// anchor model without pinning all child points. Preview callers disable
374    /// solver writeback so solved geometry can be returned without feeding every
375    /// solver value back into KCL.
376    pub async fn edit_segments_with_options(
377        &mut self,
378        ctx: &ExecutorContext,
379        version: Version,
380        sketch: ObjectId,
381        segments: Vec<ExistingSegmentCtor>,
382        options: EditSegmentsOptions,
383    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385            self.next_drag_anchor_segment_ids
386                .replace(anchor_ids.into_iter().collect())
387        });
388        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390        let previous_commit_mode = self
391            .next_edit_commits_solver_solutions
392            .replace(options.commit_solved_initial_guesses);
393        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394        if let Some(previous_anchor_ids) = previous_anchor_ids {
395            self.next_drag_anchor_segment_ids = previous_anchor_ids;
396        }
397        self.next_segment_drag_anchors = previous_drag_anchors;
398        self.next_constraint_label_edits = previous_constraint_label_edits;
399        self.next_edit_commits_solver_solutions = previous_commit_mode;
400        result
401    }
402
403    /// Edit a distance-constraint label position with optional solver writeback.
404    ///
405    /// Drag previews set `commit_solved_initial_guesses` to false so label
406    /// placement can be previewed against solved geometry without advancing
407    /// persistent KCL state until drag completion.
408    pub async fn edit_distance_constraint_label_position_with_options(
409        &mut self,
410        ctx: &ExecutorContext,
411        version: Version,
412        sketch: ObjectId,
413        constraint_id: ObjectId,
414        label_position: Point2d<Number>,
415        options: EditDistanceConstraintLabelPositionOptions,
416    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417        let previous_commit_mode = self
418            .next_edit_commits_solver_solutions
419            .replace(options.commit_solved_initial_guesses);
420        let result = SketchApi::edit_distance_constraint_label_position(
421            self,
422            ctx,
423            version,
424            sketch,
425            constraint_id,
426            label_position,
427            options.anchor_segment_ids,
428        )
429        .await;
430        self.next_edit_commits_solver_solutions = previous_commit_mode;
431        result
432    }
433
434    /// Edit a distance constraint with optional solver writeback.
435    pub async fn edit_distance_constraint_with_options(
436        &mut self,
437        ctx: &ExecutorContext,
438        version: Version,
439        sketch: ObjectId,
440        constraint_id: ObjectId,
441        constraint: Constraint,
442        options: EditConstraintOptions,
443    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445            .await
446    }
447
448    /// Edit an angle constraint with optional solver writeback.
449    pub async fn edit_angle_constraint_with_options(
450        &mut self,
451        ctx: &ExecutorContext,
452        version: Version,
453        sketch: ObjectId,
454        constraint_id: ObjectId,
455        angle: Angle,
456        options: EditConstraintOptions,
457    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459            .await
460    }
461
462    /// Edit an angle or distance-family constraint with optional solver writeback.
463    pub async fn edit_constraint_with_options(
464        &mut self,
465        ctx: &ExecutorContext,
466        _version: Version,
467        sketch: ObjectId,
468        constraint_id: ObjectId,
469        constraint: Constraint,
470        options: EditConstraintOptions,
471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472        // TODO: Check version.
473        let sketch_block_ref =
474            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478        })?;
479
480        let mut new_ast = self.program.ast.clone();
481        let command = match &object.kind {
482            ObjectKind::Constraint {
483                constraint:
484                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485            } => {
486                let (function_name, distance) = match &constraint {
487                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
488                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490                    _ => {
491                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
493                        )));
494                    }
495                };
496                let (call, value) = self
497                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498                    .map_err(KclErrorWithOutputs::no_outputs)?;
499                AstMutateCommand::EditDistanceConstraint { call, value }
500            }
501            ObjectKind::Constraint {
502                constraint: Constraint::Angle(_),
503            } => {
504                let Constraint::Angle(angle) = &constraint else {
505                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
507                    )));
508                };
509                let (call, value) = self
510                    .angle_constraint_ast_parts(angle, &mut new_ast)
511                    .map_err(KclErrorWithOutputs::no_outputs)?;
512                AstMutateCommand::EditAngleConstraint { call, value }
513            }
514            ObjectKind::Constraint { .. } => {
515                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516                    "Editing {} is not supported",
517                    object.kind.human_friendly_kind_with_article(),
518                ))));
519            }
520            _ => {
521                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522                    "Object is not a constraint: {constraint_id:?}"
523                ))));
524            }
525        };
526
527        self.mutate_ast(&mut new_ast, constraint_id, command)
528            .map_err(KclErrorWithOutputs::no_outputs)?;
529
530        self.execute_after_edit(
531            ctx,
532            sketch,
533            sketch_block_ref,
534            &mut new_ast,
535            ExecuteAfterEditOptions {
536                segment_ids_edited: Default::default(),
537                edit_kind: EditDeleteKind::Edit,
538                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539            },
540        )
541        .await
542    }
543
544    pub async fn restore_sketch_checkpoint(
545        &mut self,
546        checkpoint_id: SketchCheckpointId,
547    ) -> api::Result<RestoreSketchCheckpointOutcome> {
548        let checkpoint = self
549            .sketch_checkpoints
550            .iter()
551            .find(|checkpoint| checkpoint.id == checkpoint_id)
552            .cloned()
553            .ok_or_else(|| Error {
554                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555            })?;
556
557        self.program = checkpoint.program;
558        self.scene_graph = checkpoint.scene_graph.clone();
559        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560        self.point_freedom_cache = checkpoint.point_freedom_cache;
561        self.next_drag_anchor_segment_ids = None;
562        self.next_segment_drag_anchors = None;
563        self.next_constraint_label_edits = None;
564        self.next_edit_commits_solver_solutions = None;
565
566        if let Some(mock_memory) = checkpoint.mock_memory {
567            write_old_memory(mock_memory).await;
568        } else {
569            clear_mem_cache().await;
570        }
571
572        Ok(RestoreSketchCheckpointOutcome {
573            source_delta: checkpoint.source,
574            scene_graph_delta: SceneGraphDelta {
575                new_graph: self.scene_graph_for_ui(),
576                new_objects: Vec::new(),
577                invalidates_ids: true,
578                exec_outcome: checkpoint.exec_outcome,
579            },
580        })
581    }
582
583    pub fn clear_sketch_checkpoints(&mut self) {
584        self.sketch_checkpoints.clear();
585    }
586    fn scene_graph_for_ui(&self) -> SceneGraph {
587        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588            matches!(
589                object.kind,
590                ObjectKind::Segment {
591                    segment: Segment::ControlPointSpline(_)
592                }
593            )
594        });
595
596        if !has_control_point_splines {
597            return self.scene_graph.clone();
598        }
599
600        let hidden_constraint_ids = self
601            .scene_graph
602            .objects
603            .iter()
604            .filter_map(|object| match &object.kind {
605                ObjectKind::Constraint {
606                    constraint: Constraint::Coincident(coincident),
607                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608                    Some(object.id)
609                }
610                _ => None,
611            })
612            .collect::<HashSet<_>>();
613
614        if hidden_constraint_ids.is_empty() {
615            return self.scene_graph.clone();
616        }
617
618        let mut scene_graph = self.scene_graph.clone();
619        for object in &mut scene_graph.objects {
620            match &mut object.kind {
621                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622                    object.kind = ObjectKind::Nil;
623                }
624                ObjectKind::Sketch(sketch) => {
625                    sketch
626                        .constraints
627                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628                }
629                _ => {}
630            }
631        }
632
633        scene_graph
634    }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638    let mut first_owner_id = None;
639    for segment_id in coincident.segment_ids() {
640        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641            return false;
642        };
643
644        match first_owner_id {
645            Some(first_owner_id) if first_owner_id != owner_id => return false,
646            Some(_) => {}
647            None => first_owner_id = Some(owner_id),
648        }
649    }
650
651    first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655    let object = scene_graph.objects.get(segment_id.0)?;
656    let ObjectKind::Segment { segment } = &object.kind else {
657        return None;
658    };
659
660    match segment {
661        Segment::ControlPointSpline(_) => Some(segment_id),
662        Segment::Point(point) => point
663            .owner
664            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665        Segment::Line(line) => line
666            .owner
667            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668        _ => None,
669    }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673    matches!(
674        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675        Some(ObjectKind::Segment {
676            segment: Segment::ControlPointSpline(_)
677        })
678    )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682    let experimental_features_allowed = program
683        .meta_settings()
684        .ok()
685        .flatten()
686        .map(|settings| settings.experimental_features == WarningLevel::Allow)
687        .unwrap_or(false);
688    if experimental_features_allowed {
689        return Ok(program.clone());
690    }
691
692    program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696    async fn execute_mock(
697        &mut self,
698        ctx: &ExecutorContext,
699        _version: Version,
700        sketch: ObjectId,
701    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702        let sketch_block_ref =
703            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705        let mut truncated_program = self.program.clone();
706        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707            .map_err(KclErrorWithOutputs::no_outputs)?;
708
709        // Execute.
710        let outcome = ctx
711            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712            .await?;
713        let new_source = source_from_ast(&self.program.ast);
714        let src_delta = SourceDelta { text: new_source };
715        // MockConfig::default() has freedom_analysis: true
716        let outcome = self.update_state_after_exec(outcome, true);
717        let scene_graph_delta = SceneGraphDelta {
718            new_graph: self.scene_graph.clone(),
719            new_objects: Default::default(),
720            invalidates_ids: false,
721            exec_outcome: outcome,
722        };
723        Ok((src_delta, scene_graph_delta))
724    }
725
726    async fn new_sketch(
727        &mut self,
728        ctx: &ExecutorContext,
729        _project: ProjectId,
730        _file: FileId,
731        _version: Version,
732        args: SketchCtor,
733    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734        // TODO: Check version.
735
736        let mut new_ast = self.program.ast.clone();
737        // Create updated KCL source from args.
738        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739            .map_err(KclErrorWithOutputs::no_outputs)?;
740        let mut defined_names = find_defined_names(&new_ast);
741        let is_face_of_expr = matches!(
742            &plane_ast,
743            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744        );
745        if is_face_of_expr {
746            let face_name = next_free_name_with_padding("face", &defined_names)
747                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748            let face_decl = ast::VariableDeclaration::new(
749                ast::VariableDeclarator::new(&face_name, plane_ast),
750                ast::ItemVisibility::Default,
751                ast::VariableKind::Const,
752            );
753            new_ast
754                .body
755                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756                    face_decl,
757                ))));
758            defined_names.insert(face_name.clone());
759            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760        }
761        let sketch_ast = ast::SketchBlock {
762            arguments: vec![ast::LabeledArg {
763                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764                arg: plane_ast,
765            }],
766            body: Default::default(),
767            is_being_edited: false,
768            non_code_meta: Default::default(),
769            digest: None,
770        };
771        // Add a sketch block as a variable declaration directly, avoiding
772        // source-range mutation on a no-src node.
773        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775        let sketch_decl = ast::VariableDeclaration::new(
776            ast::VariableDeclarator::new(
777                &sketch_name,
778                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779            ),
780            ast::ItemVisibility::Default,
781            ast::VariableKind::Const,
782        );
783        new_ast
784            .body
785            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786                sketch_decl,
787            ))));
788        // Convert to string source to create real source ranges.
789        let new_source = source_from_ast(&new_ast);
790        // Parse the new source.
791        let new_program = parse_frontend_mutation_source(
792            &new_source,
793            "Error parsing KCL source after adding sketch",
794            "No AST produced after adding sketch",
795        )?;
796
797        // Make sure to only set this if there are no errors.
798        self.program = new_program.clone();
799
800        // We need to do an engine execute so that the plane object gets created
801        // and is cached.
802        let outcome = ctx.run_with_caching(new_program.clone()).await?;
803        let freedom_analysis_ran = true;
804
805        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807        let Some(sketch_id) = self
808            .scene_graph
809            .objects
810            .iter()
811            .filter_map(|object| match object.kind {
812                ObjectKind::Sketch(_) => Some(object.id),
813                _ => None,
814            })
815            .max_by_key(|id| id.0)
816        else {
817            return Err(KclErrorWithOutputs::from_error_outcome(
818                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819                outcome,
820            ));
821        };
822        // Store the object in the scene.
823        self.scene_graph.sketch_mode = Some(sketch_id);
824
825        let src_delta = SourceDelta { text: new_source };
826        let scene_graph_delta = SceneGraphDelta {
827            new_graph: self.scene_graph_for_ui(),
828            invalidates_ids: false,
829            new_objects: vec![sketch_id],
830            exec_outcome: outcome,
831        };
832        Ok((src_delta, scene_graph_delta, sketch_id))
833    }
834
835    async fn edit_sketch(
836        &mut self,
837        ctx: &ExecutorContext,
838        _project: ProjectId,
839        _file: FileId,
840        _version: Version,
841        sketch: ObjectId,
842    ) -> ExecResult<SceneGraphDelta> {
843        // TODO: Check version.
844
845        // Look up existing sketch.
846        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848        })?;
849        let ObjectKind::Sketch(_) = &sketch_object.kind else {
850            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851                "Object is not a sketch, it is {}",
852                sketch_object.kind.human_friendly_kind_with_article()
853            ))));
854        };
855        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857        // Enter sketch mode by setting the sketch_mode.
858        self.scene_graph.sketch_mode = Some(sketch);
859
860        // Truncate after the sketch block for mock execution.
861        let mut truncated_program = self.program.clone();
862        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863            .map_err(KclErrorWithOutputs::no_outputs)?;
864
865        // Execute in mock mode to ensure state is up to date. The caller will
866        // want freedom analysis to display segments correctly.
867        let outcome = ctx
868            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869            .await?;
870
871        // MockConfig::default() has freedom_analysis: true
872        let outcome = self.update_state_after_exec(outcome, true);
873        let scene_graph_delta = SceneGraphDelta {
874            new_graph: self.scene_graph_for_ui(),
875            invalidates_ids: false,
876            new_objects: Vec::new(),
877            exec_outcome: outcome,
878        };
879        Ok(scene_graph_delta)
880    }
881
882    async fn exit_sketch(
883        &mut self,
884        ctx: &ExecutorContext,
885        _version: Version,
886        sketch: ObjectId,
887    ) -> ExecResult<SceneGraph> {
888        // TODO: Check version.
889        #[cfg(not(target_arch = "wasm32"))]
890        let _ = sketch;
891        #[cfg(target_arch = "wasm32")]
892        if self.scene_graph.sketch_mode != Some(sketch) {
893            web_sys::console::warn_1(
894                &format!(
895                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896                    self.scene_graph.sketch_mode
897                )
898                .into(),
899            );
900        }
901        self.scene_graph.sketch_mode = None;
902
903        // Execute.
904        let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
907        self.update_state_after_exec(outcome, false);
908
909        Ok(self.scene_graph_for_ui())
910    }
911
912    async fn delete_sketch(
913        &mut self,
914        ctx: &ExecutorContext,
915        _version: Version,
916        sketch: ObjectId,
917    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918        // TODO: Check version.
919
920        let mut new_ast = self.program.ast.clone();
921
922        // Look up existing sketch.
923        let sketch_id = sketch;
924        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926        })?;
927        let ObjectKind::Sketch(_) = &sketch_object.kind else {
928            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929                "Object is not a sketch, it is {}",
930                sketch_object.kind.human_friendly_kind_with_article(),
931            ))));
932        };
933
934        // Modify the AST to remove the sketch.
935        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936            .map_err(KclErrorWithOutputs::no_outputs)?;
937
938        self.execute_after_delete_sketch(ctx, &mut new_ast).await
939    }
940
941    async fn add_segment(
942        &mut self,
943        ctx: &ExecutorContext,
944        _version: Version,
945        sketch: ObjectId,
946        segment: SegmentCtor,
947        _label: Option<String>,
948    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949        // TODO: Check version.
950        match segment {
951            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956        }
957    }
958
959    async fn edit_segments(
960        &mut self,
961        ctx: &ExecutorContext,
962        _version: Version,
963        sketch: ObjectId,
964        segments: Vec<ExistingSegmentCtor>,
965    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966        // TODO: Check version.
967        let sketch_block_ref =
968            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970        let mut new_ast = self.program.ast.clone();
971        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972        let mut invalidates_ids = false;
973
974        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
975        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
976        for segment in &segments {
977            edited_segment_ids.insert(segment.id);
978            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980                && let ObjectKind::Segment {
981                    segment: Segment::ControlPointSpline(existing_spline),
982                } = &existing_object.kind
983                && existing_spline.controls.len() != new_ctor.points.len()
984            {
985                invalidates_ids = true;
986            }
987        }
988        let drag_anchor_segment_ids = self
989            .next_drag_anchor_segment_ids
990            .take()
991            .unwrap_or_else(|| edited_segment_ids.clone());
992        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995        // Preprocess segments into a final_edits vector to handle if segments contains:
996        // - edit start point of line1 (as SegmentCtor::Point)
997        // - edit end point of line1 (as SegmentCtor::Point)
998        //
999        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1000        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1001        //
1002        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1003        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1004        // so the above example would result in a single line1 edit:
1005        // - the first start point edit creates a new line edit entry in final_edits
1006        // - the second end point edit finds this line edit and mutates the end position only.
1007        //
1008        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1009        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011        for segment in segments {
1012            let segment_id = segment.id;
1013            match segment.ctor {
1014                SegmentCtor::Point(ctor) => {
1015                    // Find the owner, if any (point -> line / arc)
1016                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017                        && let ObjectKind::Segment { segment } = &segment_object.kind
1018                        && let Segment::Point(point) = segment
1019                        && let Some(owner_id) = point.owner
1020                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022                    {
1023                        match owner_segment {
1024                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1026                                    let SegmentCtor::Line(line_ctor) = existing else {
1027                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028                                            "Internal: Expected line ctor for owner, but found {}",
1029                                            existing.human_friendly_kind_with_article()
1030                                        ))));
1031                                    };
1032                                    // Line owner is already in final_edits -> apply this point edit
1033                                    if line.start == segment_id {
1034                                        line_ctor.start = ctor.position;
1035                                    } else {
1036                                        line_ctor.end = ctor.position;
1037                                    }
1038                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039                                    // Line owner is not in final_edits yet -> create it
1040                                    let mut line_ctor = line_ctor.clone();
1041                                    if line.start == segment_id {
1042                                        line_ctor.start = ctor.position;
1043                                    } else {
1044                                        line_ctor.end = ctor.position;
1045                                    }
1046                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047                                } else {
1048                                    // This should never run..
1049                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050                                        "Internal: Line does not have line ctor, but found {}",
1051                                        line.ctor.human_friendly_kind_with_article()
1052                                    ))));
1053                                }
1054                                continue;
1055                            }
1056                            Segment::Arc(arc)
1057                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058                            {
1059                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1060                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1061                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062                                            "Internal: Expected arc ctor for owner, but found {}",
1063                                            existing.human_friendly_kind_with_article()
1064                                        ))));
1065                                    };
1066                                    if arc.start == segment_id {
1067                                        arc_ctor.start = ctor.position;
1068                                    } else if arc.end == segment_id {
1069                                        arc_ctor.end = ctor.position;
1070                                    } else {
1071                                        arc_ctor.center = ctor.position;
1072                                    }
1073                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074                                    let mut arc_ctor = arc_ctor.clone();
1075                                    if arc.start == segment_id {
1076                                        arc_ctor.start = ctor.position;
1077                                    } else if arc.end == segment_id {
1078                                        arc_ctor.end = ctor.position;
1079                                    } else {
1080                                        arc_ctor.center = ctor.position;
1081                                    }
1082                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083                                } else {
1084                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085                                        "Internal: Arc does not have arc ctor, but found {}",
1086                                        arc.ctor.human_friendly_kind_with_article()
1087                                    ))));
1088                                }
1089                                continue;
1090                            }
1091                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1093                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1094                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095                                            "Internal: Expected circle ctor for owner, but found {}",
1096                                            existing.human_friendly_kind_with_article()
1097                                        ))));
1098                                    };
1099                                    if circle.start == segment_id {
1100                                        circle_ctor.start = ctor.position;
1101                                    } else {
1102                                        circle_ctor.center = ctor.position;
1103                                    }
1104                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105                                    let mut circle_ctor = circle_ctor.clone();
1106                                    if circle.start == segment_id {
1107                                        circle_ctor.start = ctor.position;
1108                                    } else {
1109                                        circle_ctor.center = ctor.position;
1110                                    }
1111                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112                                } else {
1113                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114                                        "Internal: Circle does not have circle ctor, but found {}",
1115                                        circle.ctor.human_friendly_kind_with_article()
1116                                    ))));
1117                                }
1118                                continue;
1119                            }
1120                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121                                let Some(control_index) =
1122                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1123                                else {
1124                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126                                    ))));
1127                                };
1128                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1129                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132                                            existing.human_friendly_kind_with_article()
1133                                        ))));
1134                                    };
1135                                    spline_ctor.points[control_index] = ctor.position;
1136                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137                                    let mut spline_ctor = spline_ctor.clone();
1138                                    spline_ctor.points[control_index] = ctor.position;
1139                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140                                } else {
1141                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143                                        spline.ctor.human_friendly_kind_with_article()
1144                                    ))));
1145                                }
1146                                continue;
1147                            }
1148                            _ => {}
1149                        }
1150                    }
1151
1152                    // No owner, it's an individual point
1153                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154                }
1155                SegmentCtor::Line(ctor) => {
1156                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157                }
1158                SegmentCtor::Arc(ctor) => {
1159                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160                }
1161                SegmentCtor::Circle(ctor) => {
1162                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163                }
1164                SegmentCtor::ControlPointSpline(ctor) => {
1165                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166                }
1167            }
1168        }
1169
1170        for (segment_id, ctor) in final_edits {
1171            match ctor {
1172                SegmentCtor::Point(ctor) => self
1173                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174                    .map_err(KclErrorWithOutputs::no_outputs)?,
1175                SegmentCtor::Line(ctor) => self
1176                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177                    .map_err(KclErrorWithOutputs::no_outputs)?,
1178                SegmentCtor::Arc(ctor) => self
1179                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180                    .map_err(KclErrorWithOutputs::no_outputs)?,
1181                SegmentCtor::Circle(ctor) => self
1182                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183                    .map_err(KclErrorWithOutputs::no_outputs)?,
1184                SegmentCtor::ControlPointSpline(ctor) => self
1185                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186                    .map_err(KclErrorWithOutputs::no_outputs)?,
1187            }
1188        }
1189        for edit in constraint_label_edits {
1190            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191                .map_err(KclErrorWithOutputs::no_outputs)?;
1192        }
1193        let (source_delta, mut scene_graph_delta) = self
1194            .execute_after_edit(
1195                ctx,
1196                sketch,
1197                sketch_block_ref,
1198                &mut new_ast,
1199                ExecuteAfterEditOptions {
1200                    segment_ids_edited: drag_anchor_segment_ids,
1201                    edit_kind: EditDeleteKind::Edit,
1202                    commit_solved_initial_guesses,
1203                },
1204            )
1205            .await?;
1206        if invalidates_ids {
1207            scene_graph_delta.invalidates_ids = true;
1208        }
1209        Ok((source_delta, scene_graph_delta))
1210    }
1211
1212    async fn delete_objects(
1213        &mut self,
1214        ctx: &ExecutorContext,
1215        _version: Version,
1216        sketch: ObjectId,
1217        constraint_ids: Vec<ObjectId>,
1218        segment_ids: Vec<ObjectId>,
1219    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220        // TODO: Check version.
1221        let sketch_block_ref =
1222            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224        // Deduplicate IDs.
1225        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1229        // also delete the point, as well as other points that are owned by the owner.
1230        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232        for segment_id in segment_ids_set.iter().copied() {
1233            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234                let ObjectKind::Segment { segment } = &segment_object.kind else {
1235                    return None;
1236                };
1237                match segment {
1238                    Segment::Point(point) => point.owner,
1239                    Segment::Line(line) => line.owner,
1240                    _ => None,
1241                }
1242            });
1243
1244            if let Some(owner_id) = owner_id
1245                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247                && matches!(
1248                    owner_segment,
1249                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250                )
1251            {
1252                // segment is owned -> delete the owner
1253                resolved_segment_ids_to_delete.insert(owner_id);
1254            } else {
1255                // segment is not owned by anything -> can be deleted
1256                resolved_segment_ids_to_delete.insert(segment_id);
1257            }
1258        }
1259        let referenced_constraint_ids = self
1260            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261            .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263        let mut new_ast = self.program.ast.clone();
1264
1265        for constraint_id in referenced_constraint_ids {
1266            if constraint_ids_set.contains(&constraint_id) {
1267                continue;
1268            }
1269
1270            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272            })?;
1273            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275                    "Object is not a constraint, it is {}",
1276                    constraint_object.kind.human_friendly_kind_with_article()
1277                ))));
1278            };
1279
1280            match constraint {
1281                Constraint::Coincident(coincident) => {
1282                    let remaining_segments =
1283                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1286                    if remaining_segments.len() >= 2 {
1287                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288                            .map_err(KclErrorWithOutputs::no_outputs)?;
1289                    } else {
1290                        constraint_ids_set.insert(constraint_id);
1291                    }
1292                }
1293                Constraint::EqualRadius(equal_radius) => {
1294                    let remaining_input = equal_radius
1295                        .input
1296                        .iter()
1297                        .copied()
1298                        .filter(|segment_id| {
1299                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300                        })
1301                        .collect::<Vec<_>>();
1302
1303                    if remaining_input.len() >= 2 {
1304                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305                            .map_err(KclErrorWithOutputs::no_outputs)?;
1306                    } else {
1307                        constraint_ids_set.insert(constraint_id);
1308                    }
1309                }
1310                Constraint::LinesEqualLength(lines_equal_length) => {
1311                    let remaining_lines = lines_equal_length
1312                        .lines
1313                        .iter()
1314                        .copied()
1315                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316                        .collect::<Vec<_>>();
1317
1318                    // Equal length constraint is only valid with at least 2 lines
1319                    if remaining_lines.len() >= 2 {
1320                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321                            .map_err(KclErrorWithOutputs::no_outputs)?;
1322                    } else {
1323                        constraint_ids_set.insert(constraint_id);
1324                    }
1325                }
1326                Constraint::Parallel(parallel) => {
1327                    let remaining_lines = parallel
1328                        .lines
1329                        .iter()
1330                        .copied()
1331                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332                        .collect::<Vec<_>>();
1333
1334                    if remaining_lines.len() >= 2 {
1335                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336                            .map_err(KclErrorWithOutputs::no_outputs)?;
1337                    } else {
1338                        constraint_ids_set.insert(constraint_id);
1339                    }
1340                }
1341                Constraint::Horizontal(Horizontal::Points { points }) => {
1342                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344                    if remaining_points.len() >= 2 {
1345                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346                            .map_err(KclErrorWithOutputs::no_outputs)?;
1347                    } else {
1348                        constraint_ids_set.insert(constraint_id);
1349                    }
1350                }
1351                Constraint::Vertical(Vertical::Points { points }) => {
1352                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354                    if remaining_points.len() >= 2 {
1355                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356                            .map_err(KclErrorWithOutputs::no_outputs)?;
1357                    } else {
1358                        constraint_ids_set.insert(constraint_id);
1359                    }
1360                }
1361                Constraint::Fixed(fixed) => {
1362                    if fixed.points.iter().any(|fixed_point| {
1363                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364                    }) {
1365                        constraint_ids_set.insert(constraint_id);
1366                    }
1367                }
1368                _ => {
1369                    // All other constraint types: if referenced by a segment -> delete the constraint
1370                    constraint_ids_set.insert(constraint_id);
1371                }
1372            }
1373        }
1374
1375        for constraint_id in constraint_ids_set {
1376            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377                .map_err(KclErrorWithOutputs::no_outputs)?;
1378        }
1379        for segment_id in resolved_segment_ids_to_delete {
1380            self.delete_segment(&mut new_ast, sketch, segment_id)
1381                .map_err(KclErrorWithOutputs::no_outputs)?;
1382        }
1383
1384        self.execute_after_edit(
1385            ctx,
1386            sketch,
1387            sketch_block_ref,
1388            &mut new_ast,
1389            ExecuteAfterEditOptions {
1390                segment_ids_edited: Default::default(),
1391                edit_kind: EditDeleteKind::DeleteNonSketch,
1392                commit_solved_initial_guesses: true,
1393            },
1394        )
1395        .await
1396    }
1397
1398    async fn add_constraint(
1399        &mut self,
1400        ctx: &ExecutorContext,
1401        _version: Version,
1402        sketch: ObjectId,
1403        constraint: Constraint,
1404    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405        // TODO: Check version.
1406
1407        // Save the original state as a backup - we'll restore it if anything fails
1408        let original_program = self.program.clone();
1409        let original_scene_graph = self.scene_graph.clone();
1410
1411        let mut new_ast = self.program.ast.clone();
1412        let sketch_block_ref = match constraint {
1413            Constraint::Coincident(coincident) => self
1414                .add_coincident(sketch, coincident, &mut new_ast)
1415                .await
1416                .map_err(KclErrorWithOutputs::no_outputs)?,
1417            Constraint::Distance(distance) => self
1418                .add_distance(sketch, distance, &mut new_ast)
1419                .await
1420                .map_err(KclErrorWithOutputs::no_outputs)?,
1421            Constraint::EqualRadius(equal_radius) => self
1422                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423                .await
1424                .map_err(KclErrorWithOutputs::no_outputs)?,
1425            Constraint::Fixed(fixed) => self
1426                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427                .await
1428                .map_err(KclErrorWithOutputs::no_outputs)?,
1429            Constraint::HorizontalDistance(distance) => self
1430                .add_horizontal_distance(sketch, distance, &mut new_ast)
1431                .await
1432                .map_err(KclErrorWithOutputs::no_outputs)?,
1433            Constraint::VerticalDistance(distance) => self
1434                .add_vertical_distance(sketch, distance, &mut new_ast)
1435                .await
1436                .map_err(KclErrorWithOutputs::no_outputs)?,
1437            Constraint::Horizontal(horizontal) => self
1438                .add_horizontal(sketch, horizontal, &mut new_ast)
1439                .await
1440                .map_err(KclErrorWithOutputs::no_outputs)?,
1441            Constraint::LinesEqualLength(lines_equal_length) => self
1442                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443                .await
1444                .map_err(KclErrorWithOutputs::no_outputs)?,
1445            Constraint::Midpoint(midpoint) => self
1446                .add_midpoint(sketch, midpoint, &mut new_ast)
1447                .await
1448                .map_err(KclErrorWithOutputs::no_outputs)?,
1449            Constraint::Parallel(parallel) => self
1450                .add_parallel(sketch, parallel, &mut new_ast)
1451                .await
1452                .map_err(KclErrorWithOutputs::no_outputs)?,
1453            Constraint::Perpendicular(perpendicular) => self
1454                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455                .await
1456                .map_err(KclErrorWithOutputs::no_outputs)?,
1457            Constraint::Radius(radius) => self
1458                .add_radius(sketch, radius, &mut new_ast)
1459                .await
1460                .map_err(KclErrorWithOutputs::no_outputs)?,
1461            Constraint::Diameter(diameter) => self
1462                .add_diameter(sketch, diameter, &mut new_ast)
1463                .await
1464                .map_err(KclErrorWithOutputs::no_outputs)?,
1465            Constraint::Symmetric(symmetric) => self
1466                .add_symmetric(sketch, symmetric, &mut new_ast)
1467                .await
1468                .map_err(KclErrorWithOutputs::no_outputs)?,
1469            Constraint::Vertical(vertical) => self
1470                .add_vertical(sketch, vertical, &mut new_ast)
1471                .await
1472                .map_err(KclErrorWithOutputs::no_outputs)?,
1473            Constraint::Angle(lines_at_angle) => self
1474                .add_angle(sketch, lines_at_angle, &mut new_ast)
1475                .await
1476                .map_err(KclErrorWithOutputs::no_outputs)?,
1477            Constraint::Tangent(tangent) => self
1478                .add_tangent(sketch, tangent, &mut new_ast)
1479                .await
1480                .map_err(KclErrorWithOutputs::no_outputs)?,
1481        };
1482
1483        let result = self
1484            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485            .await;
1486
1487        // If execution failed, restore the original state to prevent corruption
1488        if result.is_err() {
1489            self.program = original_program;
1490            self.scene_graph = original_scene_graph;
1491        }
1492
1493        result
1494    }
1495
1496    async fn chain_segment(
1497        &mut self,
1498        ctx: &ExecutorContext,
1499        version: Version,
1500        sketch: ObjectId,
1501        previous_segment_end_point_id: ObjectId,
1502        segment: SegmentCtor,
1503        _label: Option<String>,
1504    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505        // TODO: Check version.
1506
1507        // First, add the segment (line) to get its start point ID
1508        let SegmentCtor::Line(line_ctor) = segment else {
1509            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510                "chain_segment currently only supports Line segments, got {}",
1511                segment.human_friendly_kind_with_article(),
1512            ))));
1513        };
1514
1515        // Add the line segment first - this updates self.program and self.scene_graph
1516        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518        // Find the new line's start point ID from the updated scene graph
1519        // add_line updates self.scene_graph, so we can use that
1520        let new_line_id = first_scene_delta
1521            .new_objects
1522            .iter()
1523            .find(|&obj_id| {
1524                let obj = self.scene_graph.objects.get(obj_id.0);
1525                if let Some(obj) = obj {
1526                    matches!(
1527                        &obj.kind,
1528                        ObjectKind::Segment {
1529                            segment: Segment::Line(_)
1530                        }
1531                    )
1532                } else {
1533                    false
1534                }
1535            })
1536            .ok_or_else(|| {
1537                KclErrorWithOutputs::no_outputs(KclError::refactor(
1538                    "Failed to find new line segment in scene graph".to_string(),
1539                ))
1540            })?;
1541
1542        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "New line object not found: {new_line_id:?}"
1545            )))
1546        })?;
1547
1548        let ObjectKind::Segment {
1549            segment: new_line_segment,
1550        } = &new_line_obj.kind
1551        else {
1552            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553                "Object is not a segment: {new_line_obj:?}"
1554            ))));
1555        };
1556
1557        let Segment::Line(new_line) = new_line_segment else {
1558            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559                "Segment is not a line: {new_line_segment:?}"
1560            ))));
1561        };
1562
1563        let new_line_start_point_id = new_line.start;
1564
1565        // Now add the coincident constraint between the previous end point and the new line's start point.
1566        let coincident = Coincident {
1567            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568        };
1569
1570        let (final_src_delta, final_scene_delta) = self
1571            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572            .await?;
1573
1574        // Combine new objects from the line addition and the constraint addition.
1575        // Both add_line and add_constraint now populate new_objects correctly.
1576        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577        combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579        let scene_graph_delta = SceneGraphDelta {
1580            new_graph: self.scene_graph_for_ui(),
1581            invalidates_ids: false,
1582            new_objects: combined_new_objects,
1583            exec_outcome: final_scene_delta.exec_outcome,
1584        };
1585
1586        Ok((final_src_delta, scene_graph_delta))
1587    }
1588
1589    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1590    async fn edit_constraint_value(
1591        &mut self,
1592        ctx: &ExecutorContext,
1593        _version: Version,
1594        sketch: ObjectId,
1595        constraint_id: ObjectId,
1596        value_expression: String,
1597    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598        // TODO: Check version.
1599        let sketch_block_ref =
1600            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604        })?;
1605        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607                "Object is not a constraint: {constraint_id:?}"
1608            ))));
1609        }
1610
1611        let mut new_ast = self.program.ast.clone();
1612
1613        // Parse the expression string into an AST node.
1614        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616                "Invalid constraint value",
1617                &e,
1618            )))
1619        })?;
1620        if !errors.is_empty() {
1621            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622                format_compilation_issues("Invalid constraint value", &errors),
1623            )));
1624        }
1625        let mut parsed = parsed.ok_or_else(|| {
1626            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627        })?;
1628        if parsed.ast.body.is_empty() {
1629            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630                "Empty value expression".to_string(),
1631            )));
1632        }
1633        let first = parsed.ast.body.remove(0);
1634        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636                "Value expression must be a simple expression".to_string(),
1637            )));
1638        };
1639
1640        let new_value: ast::BinaryPart = expr_stmt
1641            .inner
1642            .expression
1643            .try_into()
1644            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646        self.mutate_ast(
1647            &mut new_ast,
1648            constraint_id,
1649            AstMutateCommand::EditConstraintValue { value: new_value },
1650        )
1651        .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653        self.execute_after_edit(
1654            ctx,
1655            sketch,
1656            sketch_block_ref,
1657            &mut new_ast,
1658            ExecuteAfterEditOptions {
1659                segment_ids_edited: Default::default(),
1660                edit_kind: EditDeleteKind::Edit,
1661                commit_solved_initial_guesses: true,
1662            },
1663        )
1664        .await
1665    }
1666
1667    async fn edit_distance_constraint_label_position(
1668        &mut self,
1669        ctx: &ExecutorContext,
1670        _version: Version,
1671        sketch: ObjectId,
1672        constraint_id: ObjectId,
1673        label_position: Point2d<Number>,
1674        anchor_segment_ids: Vec<ObjectId>,
1675    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676        // TODO: Check version.
1677        let sketch_block_ref =
1678            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680        let mut new_ast = self.program.ast.clone();
1681        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682            .map_err(KclErrorWithOutputs::no_outputs)?;
1683        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685        self.execute_after_edit(
1686            ctx,
1687            sketch,
1688            sketch_block_ref,
1689            &mut new_ast,
1690            ExecuteAfterEditOptions {
1691                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692                edit_kind: EditDeleteKind::Edit,
1693                commit_solved_initial_guesses,
1694            },
1695        )
1696        .await
1697    }
1698
1699    /// Splitting a segment means creating a new segment, editing the old one, and then
1700    /// migrating a bunch of the constraints from the original segment to the new one
1701    /// (i.e. deleting them and re-adding them on the other segment).
1702    ///
1703    /// To keep this efficient we require as few executions as possible: we create the
1704    /// new segment first (to get its id), then do all edits and new constraints, and
1705    /// do all deletes at the end (since deletes invalidate ids).
1706    async fn batch_split_segment_operations(
1707        &mut self,
1708        ctx: &ExecutorContext,
1709        _version: Version,
1710        sketch: ObjectId,
1711        edit_segments: Vec<ExistingSegmentCtor>,
1712        add_constraints: Vec<Constraint>,
1713        delete_constraint_ids: Vec<ObjectId>,
1714        _new_segment_info: sketch::NewSegmentInfo,
1715    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716        // TODO: Check version.
1717        let sketch_block_ref =
1718            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720        let mut new_ast = self.program.ast.clone();
1721        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723        // Step 1: Edit segments
1724        for segment in edit_segments {
1725            segment_ids_edited.insert(segment.id);
1726            match segment.ctor {
1727                SegmentCtor::Point(ctor) => self
1728                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729                    .map_err(KclErrorWithOutputs::no_outputs)?,
1730                SegmentCtor::Line(ctor) => self
1731                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732                    .map_err(KclErrorWithOutputs::no_outputs)?,
1733                SegmentCtor::Arc(ctor) => self
1734                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735                    .map_err(KclErrorWithOutputs::no_outputs)?,
1736                SegmentCtor::Circle(ctor) => self
1737                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738                    .map_err(KclErrorWithOutputs::no_outputs)?,
1739                SegmentCtor::ControlPointSpline(ctor) => self
1740                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741                    .map_err(KclErrorWithOutputs::no_outputs)?,
1742            }
1743        }
1744
1745        // Step 2: Add all constraints
1746        for constraint in add_constraints {
1747            match constraint {
1748                Constraint::Coincident(coincident) => {
1749                    self.add_coincident(sketch, coincident, &mut new_ast)
1750                        .await
1751                        .map_err(KclErrorWithOutputs::no_outputs)?;
1752                }
1753                Constraint::Distance(distance) => {
1754                    self.add_distance(sketch, distance, &mut new_ast)
1755                        .await
1756                        .map_err(KclErrorWithOutputs::no_outputs)?;
1757                }
1758                Constraint::EqualRadius(equal_radius) => {
1759                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760                        .await
1761                        .map_err(KclErrorWithOutputs::no_outputs)?;
1762                }
1763                Constraint::Fixed(fixed) => {
1764                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765                        .await
1766                        .map_err(KclErrorWithOutputs::no_outputs)?;
1767                }
1768                Constraint::HorizontalDistance(distance) => {
1769                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770                        .await
1771                        .map_err(KclErrorWithOutputs::no_outputs)?;
1772                }
1773                Constraint::VerticalDistance(distance) => {
1774                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1775                        .await
1776                        .map_err(KclErrorWithOutputs::no_outputs)?;
1777                }
1778                Constraint::Horizontal(horizontal) => {
1779                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1780                        .await
1781                        .map_err(KclErrorWithOutputs::no_outputs)?;
1782                }
1783                Constraint::LinesEqualLength(lines_equal_length) => {
1784                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785                        .await
1786                        .map_err(KclErrorWithOutputs::no_outputs)?;
1787                }
1788                Constraint::Midpoint(midpoint) => {
1789                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                Constraint::Parallel(parallel) => {
1794                    self.add_parallel(sketch, parallel, &mut new_ast)
1795                        .await
1796                        .map_err(KclErrorWithOutputs::no_outputs)?;
1797                }
1798                Constraint::Perpendicular(perpendicular) => {
1799                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800                        .await
1801                        .map_err(KclErrorWithOutputs::no_outputs)?;
1802                }
1803                Constraint::Vertical(vertical) => {
1804                    self.add_vertical(sketch, vertical, &mut new_ast)
1805                        .await
1806                        .map_err(KclErrorWithOutputs::no_outputs)?;
1807                }
1808                Constraint::Diameter(diameter) => {
1809                    self.add_diameter(sketch, diameter, &mut new_ast)
1810                        .await
1811                        .map_err(KclErrorWithOutputs::no_outputs)?;
1812                }
1813                Constraint::Radius(radius) => {
1814                    self.add_radius(sketch, radius, &mut new_ast)
1815                        .await
1816                        .map_err(KclErrorWithOutputs::no_outputs)?;
1817                }
1818                Constraint::Symmetric(symmetric) => {
1819                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1820                        .await
1821                        .map_err(KclErrorWithOutputs::no_outputs)?;
1822                }
1823                Constraint::Angle(angle) => {
1824                    self.add_angle(sketch, angle, &mut new_ast)
1825                        .await
1826                        .map_err(KclErrorWithOutputs::no_outputs)?;
1827                }
1828                Constraint::Tangent(tangent) => {
1829                    self.add_tangent(sketch, tangent, &mut new_ast)
1830                        .await
1831                        .map_err(KclErrorWithOutputs::no_outputs)?;
1832                }
1833            }
1834        }
1835
1836        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1837        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839        let has_constraint_deletions = !constraint_ids_set.is_empty();
1840        for constraint_id in constraint_ids_set {
1841            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842                .map_err(KclErrorWithOutputs::no_outputs)?;
1843        }
1844
1845        // Step 4: Execute once at the end
1846        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1847        // But we'll manually set invalidates_ids: true if we deleted constraints
1848        let (source_delta, mut scene_graph_delta) = self
1849            .execute_after_edit(
1850                ctx,
1851                sketch,
1852                sketch_block_ref,
1853                &mut new_ast,
1854                ExecuteAfterEditOptions {
1855                    segment_ids_edited,
1856                    edit_kind: EditDeleteKind::Edit,
1857                    commit_solved_initial_guesses: true,
1858                },
1859            )
1860            .await?;
1861
1862        // If we deleted constraints, set invalidates_ids: true
1863        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1864        if has_constraint_deletions {
1865            scene_graph_delta.invalidates_ids = true;
1866        }
1867
1868        Ok((source_delta, scene_graph_delta))
1869    }
1870
1871    async fn batch_tail_cut_operations(
1872        &mut self,
1873        ctx: &ExecutorContext,
1874        _version: Version,
1875        sketch: ObjectId,
1876        edit_segments: Vec<ExistingSegmentCtor>,
1877        add_constraints: Vec<Constraint>,
1878        delete_constraint_ids: Vec<ObjectId>,
1879        additional_edited_segment_ids: Vec<ObjectId>,
1880    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881        let sketch_block_ref =
1882            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884        let mut new_ast = self.program.ast.clone();
1885        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887        // Step 1: Edit segments (usually a single segment for tail cut)
1888        for segment in edit_segments {
1889            segment_ids_edited.insert(segment.id);
1890            match segment.ctor {
1891                SegmentCtor::Point(ctor) => self
1892                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893                    .map_err(KclErrorWithOutputs::no_outputs)?,
1894                SegmentCtor::Line(ctor) => self
1895                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896                    .map_err(KclErrorWithOutputs::no_outputs)?,
1897                SegmentCtor::Arc(ctor) => self
1898                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899                    .map_err(KclErrorWithOutputs::no_outputs)?,
1900                SegmentCtor::Circle(ctor) => self
1901                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902                    .map_err(KclErrorWithOutputs::no_outputs)?,
1903                SegmentCtor::ControlPointSpline(ctor) => self
1904                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905                    .map_err(KclErrorWithOutputs::no_outputs)?,
1906            }
1907        }
1908
1909        segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911        // Step 2: Add coincident constraints
1912        for constraint in add_constraints {
1913            match constraint {
1914                Constraint::Coincident(coincident) => {
1915                    self.add_coincident(sketch, coincident, &mut new_ast)
1916                        .await
1917                        .map_err(KclErrorWithOutputs::no_outputs)?;
1918                }
1919                other => {
1920                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921                        "unsupported constraint in tail cut batch: {other:?}"
1922                    ))));
1923                }
1924            }
1925        }
1926
1927        // Step 3: Delete constraints (if any)
1928        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930        let has_constraint_deletions = !constraint_ids_set.is_empty();
1931        for constraint_id in constraint_ids_set {
1932            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933                .map_err(KclErrorWithOutputs::no_outputs)?;
1934        }
1935
1936        // Step 4: Single execute_after_edit
1937        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1938        // But we'll manually set invalidates_ids: true if we deleted constraints
1939        let (source_delta, mut scene_graph_delta) = self
1940            .execute_after_edit(
1941                ctx,
1942                sketch,
1943                sketch_block_ref,
1944                &mut new_ast,
1945                ExecuteAfterEditOptions {
1946                    segment_ids_edited,
1947                    edit_kind: EditDeleteKind::Edit,
1948                    commit_solved_initial_guesses: true,
1949                },
1950            )
1951            .await?;
1952
1953        // If we deleted constraints, set invalidates_ids: true
1954        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1955        if has_constraint_deletions {
1956            scene_graph_delta.invalidates_ids = true;
1957        }
1958
1959        Ok((source_delta, scene_graph_delta))
1960    }
1961}
1962
1963impl FrontendState {
1964    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965        self.program = program.clone();
1966
1967        // Execute so that the objects are updated and available for the next
1968        // API call.
1969        // This always uses engine execution (not mock) so that things are cached.
1970        // Engine execution now runs freedom analysis automatically.
1971        // Keep existing checkpoints alive here. History may still reference
1972        // older committed sketch states across a direct-edit boundary, and a
1973        // checkpoint restore is a full state replacement anyway. We append a
1974        // fresh baseline checkpoint after the full execution below.
1975        // Clear the freedom cache since IDs might have changed after direct editing
1976        // and we're about to run freedom analysis which will repopulate it.
1977        self.point_freedom_cache.clear();
1978        match ctx.run_with_caching(program).await {
1979            Ok(outcome) => {
1980                let outcome = self.update_state_after_exec(outcome, true);
1981                let checkpoint_id = self
1982                    .create_sketch_checkpoint(outcome.clone())
1983                    .await
1984                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985                Ok(SetProgramOutcome::Success {
1986                    scene_graph: Box::new(self.scene_graph_for_ui()),
1987                    exec_outcome: Box::new(outcome),
1988                    checkpoint_id: Some(checkpoint_id),
1989                })
1990            }
1991            Err(mut err) => {
1992                // Don't return an error just because execution failed. Instead,
1993                // update state as much as possible.
1994                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995                self.update_state_after_exec(outcome, true);
1996                err.scene_graph = Some(self.scene_graph_for_ui());
1997                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998            }
1999        }
2000    }
2001
2002    /// Decorate engine execution such that our state is updated and the scene
2003    /// graph is added to the return.
2004    pub async fn engine_execute(
2005        &mut self,
2006        ctx: &ExecutorContext,
2007        program: Program,
2008    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009        self.program = program.clone();
2010
2011        // Engine execution now runs freedom analysis automatically. Clear the
2012        // freedom cache since IDs might have changed after direct editing, and
2013        // we're about to run freedom analysis which will repopulate it.
2014        self.point_freedom_cache.clear();
2015        match ctx.run_with_caching(program).await {
2016            Ok(outcome) => {
2017                let outcome = self.update_state_after_exec(outcome, true);
2018                Ok(SceneGraphDelta {
2019                    new_graph: self.scene_graph_for_ui(),
2020                    exec_outcome: outcome,
2021                    // We don't know what the new objects are.
2022                    new_objects: Default::default(),
2023                    // We don't know if IDs were invalidated.
2024                    invalidates_ids: Default::default(),
2025                })
2026            }
2027            Err(mut err) => {
2028                // Update state as much as possible, even when there's an error.
2029                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030                self.update_state_after_exec(outcome, true);
2031                err.scene_graph = Some(self.scene_graph_for_ui());
2032                Err(err)
2033            }
2034        }
2035    }
2036
2037    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038        if matches!(err.error, KclError::EngineHangup { .. }) {
2039            // It's not ideal to special-case this, but this error is very
2040            // common during development, and it causes confusing downstream
2041            // errors that have nothing to do with the actual problem.
2042            return Err(err);
2043        }
2044
2045        let KclErrorWithOutputs {
2046            error,
2047            mut non_fatal,
2048            variables,
2049            operations,
2050            artifact_graph,
2051            scene_objects,
2052            source_range_to_object,
2053            var_solutions,
2054            refactor_metadata,
2055            filenames,
2056            source_files,
2057            default_planes,
2058            ..
2059        } = err;
2060
2061        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063        Ok(ExecOutcome {
2064            variables,
2065            #[cfg(test)]
2066            test_program_memory: Default::default(),
2067            filenames,
2068            operations,
2069            artifact_graph,
2070            scene_objects,
2071            source_range_to_object,
2072            var_solutions,
2073            refactor_metadata,
2074            issues: non_fatal,
2075            source_files,
2076            default_planes,
2077        })
2078    }
2079
2080    async fn add_point(
2081        &mut self,
2082        ctx: &ExecutorContext,
2083        sketch: ObjectId,
2084        ctor: PointCtor,
2085    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2086        // Create updated KCL source from args.
2087        let at_ast = to_ast_point2d(&ctor.position)
2088            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2089        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2090            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2091            unlabeled: None,
2092            arguments: vec![ast::LabeledArg {
2093                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2094                arg: at_ast,
2095            }],
2096            digest: None,
2097            non_code_meta: Default::default(),
2098        })));
2099
2100        // Look up existing sketch.
2101        let sketch_id = sketch;
2102        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103            #[cfg(target_arch = "wasm32")]
2104            web_sys::console::error_1(
2105                &format!(
2106                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2107                    self.scene_graph.objects
2108                )
2109                .into(),
2110            );
2111            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2112        })?;
2113        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2114            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2115                "Object is not a sketch, it is {}",
2116                sketch_object.kind.human_friendly_kind_with_article(),
2117            ))));
2118        };
2119        // Add the point to the AST of the sketch block.
2120        let mut new_ast = self.program.ast.clone();
2121        let (sketch_block_ref, _) = self
2122            .mutate_ast(
2123                &mut new_ast,
2124                sketch_id,
2125                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2126            )
2127            .map_err(KclErrorWithOutputs::no_outputs)?;
2128        // Convert to string source to create real source ranges.
2129        let new_source = source_from_ast(&new_ast);
2130        // Parse the new KCL source.
2131        let new_program = parse_frontend_mutation_source(
2132            &new_source,
2133            "Error parsing KCL source after adding point",
2134            "No AST produced after adding point",
2135        )?;
2136
2137        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2138            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2139                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2140            )))
2141        })?;
2142
2143        // Make sure to only set this if there are no errors.
2144        self.program = new_program.clone();
2145
2146        // Truncate after the sketch block for mock execution.
2147        let mut truncated_program = new_program;
2148        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2149            .map_err(KclErrorWithOutputs::no_outputs)?;
2150
2151        // Execute.
2152        let outcome = ctx
2153            .run_mock(
2154                &truncated_program,
2155                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2156            )
2157            .await?;
2158
2159        let new_object_ids = {
2160            let make_err =
2161                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2162            let segment_id = outcome
2163                .source_range_to_object
2164                .get(&point_node_ref.range)
2165                .copied()
2166                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2167            let segment_object = outcome
2168                .scene_objects
2169                .get(segment_id.0)
2170                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2171            let ObjectKind::Segment { segment } = &segment_object.kind else {
2172                return Err(make_err(format!(
2173                    "Object is not a segment, it is {}",
2174                    segment_object.kind.human_friendly_kind_with_article()
2175                )));
2176            };
2177            let Segment::Point(_) = segment else {
2178                return Err(make_err(format!(
2179                    "Segment is not a point, it is {}",
2180                    segment.human_friendly_kind_with_article()
2181                )));
2182            };
2183            vec![segment_id]
2184        };
2185        let src_delta = SourceDelta { text: new_source };
2186        // Uses .no_freedom_analysis() so freedom_analysis: false
2187        let outcome = self.update_state_after_exec(outcome, false);
2188        let scene_graph_delta = SceneGraphDelta {
2189            new_graph: self.scene_graph_for_ui(),
2190            invalidates_ids: false,
2191            new_objects: new_object_ids,
2192            exec_outcome: outcome,
2193        };
2194        Ok((src_delta, scene_graph_delta))
2195    }
2196
2197    async fn add_line(
2198        &mut self,
2199        ctx: &ExecutorContext,
2200        sketch: ObjectId,
2201        ctor: LineCtor,
2202    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2203        // Create updated KCL source from args.
2204        let start_ast = to_ast_point2d(&ctor.start)
2205            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206        let end_ast = to_ast_point2d(&ctor.end)
2207            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208        let mut arguments = vec![
2209            ast::LabeledArg {
2210                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2211                arg: start_ast,
2212            },
2213            ast::LabeledArg {
2214                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2215                arg: end_ast,
2216            },
2217        ];
2218        // Add construction kwarg if construction is Some(true)
2219        if ctor.construction == Some(true) {
2220            arguments.push(ast::LabeledArg {
2221                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2222                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2223                    value: ast::LiteralValue::Bool(true),
2224                    raw: "true".to_string(),
2225                    digest: None,
2226                }))),
2227            });
2228        }
2229        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2230            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2231            unlabeled: None,
2232            arguments,
2233            digest: None,
2234            non_code_meta: Default::default(),
2235        })));
2236
2237        // Look up existing sketch.
2238        let sketch_id = sketch;
2239        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2240            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2241        })?;
2242        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2243            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2244                "Object is not a sketch, it is {}",
2245                sketch_object.kind.human_friendly_kind_with_article(),
2246            ))));
2247        };
2248        // Add the line to the AST of the sketch block.
2249        let mut new_ast = self.program.ast.clone();
2250        let (sketch_block_ref, _) = self
2251            .mutate_ast(
2252                &mut new_ast,
2253                sketch_id,
2254                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2255            )
2256            .map_err(KclErrorWithOutputs::no_outputs)?;
2257        // Convert to string source to create real source ranges.
2258        let new_source = source_from_ast(&new_ast);
2259        // Parse the new KCL source.
2260        let new_program = parse_frontend_mutation_source(
2261            &new_source,
2262            "Error parsing KCL source after adding line",
2263            "No AST produced after adding line",
2264        )?;
2265
2266        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2267            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2269            )))
2270        })?;
2271
2272        // Make sure to only set this if there are no errors.
2273        self.program = new_program.clone();
2274
2275        // Truncate after the sketch block for mock execution.
2276        let mut truncated_program = new_program;
2277        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2278            .map_err(KclErrorWithOutputs::no_outputs)?;
2279
2280        // Execute.
2281        let outcome = ctx
2282            .run_mock(
2283                &truncated_program,
2284                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2285            )
2286            .await?;
2287
2288        let new_object_ids = {
2289            let make_err =
2290                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2291            let segment_id = outcome
2292                .source_range_to_object
2293                .get(&line_node_ref.range)
2294                .copied()
2295                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2296            let segment_object = outcome
2297                .scene_object_by_id(segment_id)
2298                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2299            let ObjectKind::Segment { segment } = &segment_object.kind else {
2300                return Err(make_err(format!(
2301                    "Object is not a segment, it is {}",
2302                    segment_object.kind.human_friendly_kind_with_article()
2303                )));
2304            };
2305            let Segment::Line(line) = segment else {
2306                return Err(make_err(format!(
2307                    "Segment is not a line, it is {}",
2308                    segment.human_friendly_kind_with_article()
2309                )));
2310            };
2311            vec![line.start, line.end, segment_id]
2312        };
2313        let src_delta = SourceDelta { text: new_source };
2314        // Uses .no_freedom_analysis() so freedom_analysis: false
2315        let outcome = self.update_state_after_exec(outcome, false);
2316        let scene_graph_delta = SceneGraphDelta {
2317            new_graph: self.scene_graph_for_ui(),
2318            invalidates_ids: false,
2319            new_objects: new_object_ids,
2320            exec_outcome: outcome,
2321        };
2322        Ok((src_delta, scene_graph_delta))
2323    }
2324
2325    async fn add_arc(
2326        &mut self,
2327        ctx: &ExecutorContext,
2328        sketch: ObjectId,
2329        ctor: ArcCtor,
2330    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2331        // Create updated KCL source from args.
2332        let start_ast = to_ast_point2d(&ctor.start)
2333            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334        let end_ast = to_ast_point2d(&ctor.end)
2335            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336        let center_ast = to_ast_point2d(&ctor.center)
2337            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2338        let mut arguments = vec![
2339            ast::LabeledArg {
2340                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2341                arg: start_ast,
2342            },
2343            ast::LabeledArg {
2344                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2345                arg: end_ast,
2346            },
2347            ast::LabeledArg {
2348                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2349                arg: center_ast,
2350            },
2351        ];
2352        // Add direction kwarg if it's clockwise, since counterclockwise is the
2353        // default.
2354        if ctor.direction == Some(ArcDirection::Cw) {
2355            arguments.push(ast::LabeledArg {
2356                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2357                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2358            });
2359        }
2360        // Add construction kwarg if construction is Some(true)
2361        if ctor.construction == Some(true) {
2362            arguments.push(ast::LabeledArg {
2363                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2364                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2365                    value: ast::LiteralValue::Bool(true),
2366                    raw: "true".to_string(),
2367                    digest: None,
2368                }))),
2369            });
2370        }
2371        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2372            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2373            unlabeled: None,
2374            arguments,
2375            digest: None,
2376            non_code_meta: Default::default(),
2377        })));
2378
2379        // Look up existing sketch.
2380        let sketch_id = sketch;
2381        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2382            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2383        })?;
2384        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2385            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2386                "Object is not a sketch, it is {}",
2387                sketch_object.kind.human_friendly_kind_with_article(),
2388            ))));
2389        };
2390        // Add the arc to the AST of the sketch block.
2391        let mut new_ast = self.program.ast.clone();
2392        let (sketch_block_ref, _) = self
2393            .mutate_ast(
2394                &mut new_ast,
2395                sketch_id,
2396                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2397            )
2398            .map_err(KclErrorWithOutputs::no_outputs)?;
2399        // Convert to string source to create real source ranges.
2400        let new_source = source_from_ast(&new_ast);
2401        // Parse the new KCL source.
2402        let new_program = parse_frontend_mutation_source(
2403            &new_source,
2404            "Error parsing KCL source after adding arc",
2405            "No AST produced after adding arc",
2406        )?;
2407
2408        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2409            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2411            )))
2412        })?;
2413
2414        // Make sure to only set this if there are no errors.
2415        self.program = new_program.clone();
2416
2417        // Truncate after the sketch block for mock execution.
2418        let mut truncated_program = new_program;
2419        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2420            .map_err(KclErrorWithOutputs::no_outputs)?;
2421
2422        // Execute.
2423        let outcome = ctx
2424            .run_mock(
2425                &truncated_program,
2426                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2427            )
2428            .await?;
2429
2430        let new_object_ids = {
2431            let make_err =
2432                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2433            let segment_id = outcome
2434                .source_range_to_object
2435                .get(&arc_node_ref.range)
2436                .copied()
2437                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2438            let segment_object = outcome
2439                .scene_objects
2440                .get(segment_id.0)
2441                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2442            let ObjectKind::Segment { segment } = &segment_object.kind else {
2443                return Err(make_err(format!(
2444                    "Object is not a segment, it is {}",
2445                    segment_object.kind.human_friendly_kind_with_article()
2446                )));
2447            };
2448            let Segment::Arc(arc) = segment else {
2449                return Err(make_err(format!(
2450                    "Segment is not an arc, it is {}",
2451                    segment.human_friendly_kind_with_article()
2452                )));
2453            };
2454            vec![arc.start, arc.end, arc.center, segment_id]
2455        };
2456        let src_delta = SourceDelta { text: new_source };
2457        // Uses .no_freedom_analysis() so freedom_analysis: false
2458        let outcome = self.update_state_after_exec(outcome, false);
2459        let scene_graph_delta = SceneGraphDelta {
2460            new_graph: self.scene_graph_for_ui(),
2461            invalidates_ids: false,
2462            new_objects: new_object_ids,
2463            exec_outcome: outcome,
2464        };
2465        Ok((src_delta, scene_graph_delta))
2466    }
2467
2468    async fn add_circle(
2469        &mut self,
2470        ctx: &ExecutorContext,
2471        sketch: ObjectId,
2472        ctor: CircleCtor,
2473    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2474        // Create updated KCL source from args.
2475        let start_ast = to_ast_point2d(&ctor.start)
2476            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477        let center_ast = to_ast_point2d(&ctor.center)
2478            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2479        let mut arguments = vec![
2480            ast::LabeledArg {
2481                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2482                arg: start_ast,
2483            },
2484            ast::LabeledArg {
2485                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2486                arg: center_ast,
2487            },
2488        ];
2489        // Add construction kwarg if construction is Some(true)
2490        if ctor.construction == Some(true) {
2491            arguments.push(ast::LabeledArg {
2492                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2493                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2494                    value: ast::LiteralValue::Bool(true),
2495                    raw: "true".to_string(),
2496                    digest: None,
2497                }))),
2498            });
2499        }
2500        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2501            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2502            unlabeled: None,
2503            arguments,
2504            digest: None,
2505            non_code_meta: Default::default(),
2506        })));
2507
2508        // Look up existing sketch.
2509        let sketch_id = sketch;
2510        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2511            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2512        })?;
2513        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2514            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2515                "Object is not a sketch, it is {}",
2516                sketch_object.kind.human_friendly_kind_with_article(),
2517            ))));
2518        };
2519        // Add the circle to the AST of the sketch block.
2520        let mut new_ast = self.program.ast.clone();
2521        let (sketch_block_ref, _) = self
2522            .mutate_ast(
2523                &mut new_ast,
2524                sketch_id,
2525                AstMutateCommand::AddSketchBlockVarDecl {
2526                    prefix: CIRCLE_VARIABLE.to_owned(),
2527                    expr: circle_ast,
2528                },
2529            )
2530            .map_err(KclErrorWithOutputs::no_outputs)?;
2531        // Convert to string source to create real source ranges.
2532        let new_source = source_from_ast(&new_ast);
2533        // Parse the new KCL source.
2534        let new_program = parse_frontend_mutation_source(
2535            &new_source,
2536            "Error parsing KCL source after adding circle",
2537            "No AST produced after adding circle",
2538        )?;
2539
2540        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2541            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2542                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2543            )))
2544        })?;
2545
2546        // Make sure to only set this if there are no errors.
2547        self.program = new_program.clone();
2548
2549        // Truncate after the sketch block for mock execution.
2550        let mut truncated_program = new_program;
2551        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2552            .map_err(KclErrorWithOutputs::no_outputs)?;
2553
2554        // Execute.
2555        let outcome = ctx
2556            .run_mock(
2557                &truncated_program,
2558                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2559            )
2560            .await?;
2561
2562        let new_object_ids = {
2563            let make_err =
2564                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2565            let segment_id = outcome
2566                .source_range_to_object
2567                .get(&circle_node_ref.range)
2568                .copied()
2569                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2570            let segment_object = outcome
2571                .scene_objects
2572                .get(segment_id.0)
2573                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2574            let ObjectKind::Segment { segment } = &segment_object.kind else {
2575                return Err(make_err(format!(
2576                    "Object is not a segment, it is {}",
2577                    segment_object.kind.human_friendly_kind_with_article()
2578                )));
2579            };
2580            let Segment::Circle(circle) = segment else {
2581                return Err(make_err(format!(
2582                    "Segment is not a circle, it is {}",
2583                    segment.human_friendly_kind_with_article()
2584                )));
2585            };
2586            vec![circle.start, circle.center, segment_id]
2587        };
2588        let src_delta = SourceDelta { text: new_source };
2589        // Uses .no_freedom_analysis() so freedom_analysis: false
2590        let outcome = self.update_state_after_exec(outcome, false);
2591        let scene_graph_delta = SceneGraphDelta {
2592            new_graph: self.scene_graph_for_ui(),
2593            invalidates_ids: false,
2594            new_objects: new_object_ids,
2595            exec_outcome: outcome,
2596        };
2597        Ok((src_delta, scene_graph_delta))
2598    }
2599
2600    async fn add_control_point_spline(
2601        &mut self,
2602        ctx: &ExecutorContext,
2603        sketch: ObjectId,
2604        ctor: ControlPointSplineCtor,
2605    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2606        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2607            .map_err(KclErrorWithOutputs::no_outputs)?;
2608
2609        let points_ast = to_ast_point2d_array(&ctor.points)
2610            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2611        let mut arguments = vec![ast::LabeledArg {
2612            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2613            arg: points_ast,
2614        }];
2615        if ctor.construction == Some(true) {
2616            arguments.push(ast::LabeledArg {
2617                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2618                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2619                    value: ast::LiteralValue::Bool(true),
2620                    raw: "true".to_string(),
2621                    digest: None,
2622                }))),
2623            });
2624        }
2625        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2626            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2627            unlabeled: None,
2628            arguments,
2629            digest: None,
2630            non_code_meta: Default::default(),
2631        })));
2632
2633        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2634            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2635        })?;
2636        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2637            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2638                "Object is not a sketch, it is {}",
2639                sketch_object.kind.human_friendly_kind_with_article(),
2640            ))));
2641        };
2642
2643        let mut new_ast = new_program.ast.clone();
2644        let (sketch_block_ref, _) = self
2645            .mutate_ast(
2646                &mut new_ast,
2647                sketch,
2648                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2649            )
2650            .map_err(KclErrorWithOutputs::no_outputs)?;
2651        let new_source = source_from_ast(&new_ast);
2652        let new_program = parse_frontend_mutation_source(
2653            &new_source,
2654            "Error parsing KCL source after adding controlPointSpline",
2655            "No AST produced after adding controlPointSpline",
2656        )?;
2657
2658        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2659            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2661            )))
2662        })?;
2663
2664        self.program = new_program.clone();
2665
2666        let mut truncated_program = new_program;
2667        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2668            .map_err(KclErrorWithOutputs::no_outputs)?;
2669
2670        let outcome = ctx
2671            .run_mock(
2672                &truncated_program,
2673                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2674            )
2675            .await?;
2676
2677        let new_object_ids = {
2678            let make_err =
2679                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2680            let segment_id = outcome
2681                .source_range_to_object
2682                .get(&spline_node_ref.range)
2683                .copied()
2684                .ok_or_else(|| {
2685                    make_err(format!(
2686                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2687                    ))
2688                })?;
2689            let segment_object = outcome
2690                .scene_objects
2691                .get(segment_id.0)
2692                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2693            let ObjectKind::Segment { segment } = &segment_object.kind else {
2694                return Err(make_err(format!(
2695                    "Object is not a segment, it is {}",
2696                    segment_object.kind.human_friendly_kind_with_article()
2697                )));
2698            };
2699            let Segment::ControlPointSpline(spline) = segment else {
2700                return Err(make_err(format!(
2701                    "Segment is not a control point spline, it is {}",
2702                    segment.human_friendly_kind_with_article()
2703                )));
2704            };
2705
2706            let mut ids = outcome
2707                .scene_objects
2708                .iter()
2709                .filter_map(|obj| match &obj.kind {
2710                    ObjectKind::Segment {
2711                        segment: Segment::Line(line),
2712                    } if line.owner == Some(segment_id) => Some(obj.id),
2713                    _ => None,
2714                })
2715                .collect::<Vec<_>>();
2716            ids.extend(spline.controls.clone());
2717            ids.push(segment_id);
2718            ids
2719        };
2720        let src_delta = SourceDelta { text: new_source };
2721        let outcome = self.update_state_after_exec(outcome, false);
2722        let scene_graph_delta = SceneGraphDelta {
2723            new_graph: self.scene_graph_for_ui(),
2724            invalidates_ids: false,
2725            new_objects: new_object_ids,
2726            exec_outcome: outcome,
2727        };
2728        Ok((src_delta, scene_graph_delta))
2729    }
2730
2731    fn edit_point(
2732        &mut self,
2733        new_ast: &mut ast::Node<ast::Program>,
2734        sketch: ObjectId,
2735        point: ObjectId,
2736        ctor: PointCtor,
2737    ) -> Result<(), KclError> {
2738        // Create updated KCL source from args.
2739        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2740
2741        // Look up existing sketch.
2742        let sketch_id = sketch;
2743        let sketch_object = self
2744            .scene_graph
2745            .objects
2746            .get(sketch_id.0)
2747            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2748        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2749            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2750        };
2751        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2752            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2753        })?;
2754        // Look up existing point.
2755        let point_id = point;
2756        let point_object = self
2757            .scene_graph
2758            .objects
2759            .get(point_id.0)
2760            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2761        let ObjectKind::Segment {
2762            segment: Segment::Point(point),
2763        } = &point_object.kind
2764        else {
2765            return Err(KclError::refactor(format!(
2766                "Object is not a point segment: {point_object:?}"
2767            )));
2768        };
2769
2770        // If the point is part of a line or arc, edit the line/arc instead.
2771        if let Some(owner_id) = point.owner {
2772            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2773                KclError::refactor(format!(
2774                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2775                ))
2776            })?;
2777            let ObjectKind::Segment { segment } = &owner_object.kind else {
2778                return Err(KclError::refactor(format!(
2779                    "Internal: Owner of point is not a segment, but found {}",
2780                    owner_object.kind.human_friendly_kind_with_article()
2781                )));
2782            };
2783
2784            // Handle Line owner
2785            if let Segment::Line(line) = segment {
2786                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2787                    return Err(KclError::refactor(format!(
2788                        "Internal: Owner of point does not have line ctor, but found {}",
2789                        line.ctor.human_friendly_kind_with_article()
2790                    )));
2791                };
2792                let mut line_ctor = line_ctor.clone();
2793                // Which end of the line is this point?
2794                if line.start == point_id {
2795                    line_ctor.start = ctor.position;
2796                } else if line.end == point_id {
2797                    line_ctor.end = ctor.position;
2798                } else {
2799                    return Err(KclError::refactor(format!(
2800                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2801                    )));
2802                }
2803                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2804            }
2805
2806            // Handle Arc owner
2807            if let Segment::Arc(arc) = segment {
2808                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2809                    return Err(KclError::refactor(format!(
2810                        "Internal: Owner of point does not have arc ctor, but found {}",
2811                        arc.ctor.human_friendly_kind_with_article()
2812                    )));
2813                };
2814                let mut arc_ctor = arc_ctor.clone();
2815                // Which point of the arc is this? (center, start, or end)
2816                if arc.center == point_id {
2817                    arc_ctor.center = ctor.position;
2818                } else if arc.start == point_id {
2819                    arc_ctor.start = ctor.position;
2820                } else if arc.end == point_id {
2821                    arc_ctor.end = ctor.position;
2822                } else {
2823                    return Err(KclError::refactor(format!(
2824                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2825                    )));
2826                }
2827                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2828            }
2829
2830            // Handle Circle owner
2831            if let Segment::Circle(circle) = segment {
2832                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2833                    return Err(KclError::refactor(format!(
2834                        "Internal: Owner of point does not have circle ctor, but found {}",
2835                        circle.ctor.human_friendly_kind_with_article()
2836                    )));
2837                };
2838                let mut circle_ctor = circle_ctor.clone();
2839                if circle.center == point_id {
2840                    circle_ctor.center = ctor.position;
2841                } else if circle.start == point_id {
2842                    circle_ctor.start = ctor.position;
2843                } else {
2844                    return Err(KclError::refactor(format!(
2845                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2846                    )));
2847                }
2848                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2849            }
2850
2851            if let Segment::ControlPointSpline(spline) = segment {
2852                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2853                    return Err(KclError::refactor(format!(
2854                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2855                        spline.ctor.human_friendly_kind_with_article()
2856                    )));
2857                };
2858                let mut spline_ctor = spline_ctor.clone();
2859                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2860                    return Err(KclError::refactor(format!(
2861                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2862                    )));
2863                };
2864                spline_ctor.points[control_index] = ctor.position;
2865                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2866            }
2867
2868            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2869            // (fall through to the point editing logic below)
2870        }
2871
2872        // Modify the point AST.
2873        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2874        Ok(())
2875    }
2876
2877    fn edit_line(
2878        &mut self,
2879        new_ast: &mut ast::Node<ast::Program>,
2880        sketch: ObjectId,
2881        line: ObjectId,
2882        ctor: LineCtor,
2883    ) -> Result<(), KclError> {
2884        // Create updated KCL source from args.
2885        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2886        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2887
2888        // Look up existing sketch.
2889        let sketch_id = sketch;
2890        let sketch_object = self
2891            .scene_graph
2892            .objects
2893            .get(sketch_id.0)
2894            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897        };
2898        sketch
2899            .segments
2900            .iter()
2901            .find(|o| **o == line)
2902            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2903        // Look up existing line.
2904        let line_id = line;
2905        let line_object = self
2906            .scene_graph
2907            .objects
2908            .get(line_id.0)
2909            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2910        let ObjectKind::Segment { .. } = &line_object.kind else {
2911            let kind = line_object.kind.human_friendly_kind_with_article();
2912            return Err(KclError::refactor(format!(
2913                "This constraint only works on Segments, but you selected {kind}"
2914            )));
2915        };
2916
2917        // Modify the line AST.
2918        self.mutate_ast(
2919            new_ast,
2920            line_id,
2921            AstMutateCommand::EditLine {
2922                start: new_start_ast,
2923                end: new_end_ast,
2924                construction: ctor.construction,
2925            },
2926        )?;
2927        Ok(())
2928    }
2929
2930    fn edit_arc(
2931        &mut self,
2932        new_ast: &mut ast::Node<ast::Program>,
2933        sketch: ObjectId,
2934        arc: ObjectId,
2935        ctor: ArcCtor,
2936    ) -> Result<(), KclError> {
2937        // Create updated KCL source from args.
2938        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2939        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2940        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942        // Look up existing sketch.
2943        let sketch_id = sketch;
2944        let sketch_object = self
2945            .scene_graph
2946            .objects
2947            .get(sketch_id.0)
2948            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951        };
2952        sketch
2953            .segments
2954            .iter()
2955            .find(|o| **o == arc)
2956            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2957        // Look up existing arc.
2958        let arc_id = arc;
2959        let arc_object = self
2960            .scene_graph
2961            .objects
2962            .get(arc_id.0)
2963            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2964        let ObjectKind::Segment { .. } = &arc_object.kind else {
2965            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2966        };
2967
2968        // Modify the arc AST.
2969        self.mutate_ast(
2970            new_ast,
2971            arc_id,
2972            AstMutateCommand::EditArc {
2973                start: new_start_ast,
2974                end: new_end_ast,
2975                center: new_center_ast,
2976                direction: ctor.direction,
2977                construction: ctor.construction,
2978            },
2979        )?;
2980        Ok(())
2981    }
2982
2983    fn edit_circle(
2984        &mut self,
2985        new_ast: &mut ast::Node<ast::Program>,
2986        sketch: ObjectId,
2987        circle: ObjectId,
2988        ctor: CircleCtor,
2989    ) -> Result<(), KclError> {
2990        // Create updated KCL source from args.
2991        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2992        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2993
2994        // Look up existing sketch.
2995        let sketch_id = sketch;
2996        let sketch_object = self
2997            .scene_graph
2998            .objects
2999            .get(sketch_id.0)
3000            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3001        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3002            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3003        };
3004        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3005            KclError::refactor(format!(
3006                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3007            ))
3008        })?;
3009        // Look up existing circle.
3010        let circle_id = circle;
3011        let circle_object = self
3012            .scene_graph
3013            .objects
3014            .get(circle_id.0)
3015            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3016        let ObjectKind::Segment { .. } = &circle_object.kind else {
3017            return Err(KclError::refactor(format!(
3018                "Object is not a segment: {circle_object:?}"
3019            )));
3020        };
3021
3022        // Modify the circle AST.
3023        self.mutate_ast(
3024            new_ast,
3025            circle_id,
3026            AstMutateCommand::EditCircle {
3027                start: new_start_ast,
3028                center: new_center_ast,
3029                construction: ctor.construction,
3030            },
3031        )?;
3032        Ok(())
3033    }
3034
3035    fn edit_control_point_spline(
3036        &mut self,
3037        new_ast: &mut ast::Node<ast::Program>,
3038        sketch: ObjectId,
3039        spline: ObjectId,
3040        ctor: ControlPointSplineCtor,
3041    ) -> Result<(), KclError> {
3042        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3043
3044        let sketch_object = self
3045            .scene_graph
3046            .objects
3047            .get(sketch.0)
3048            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3049        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3050            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3051        };
3052        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3053            KclError::refactor(format!(
3054                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3055            ))
3056        })?;
3057
3058        let spline_object =
3059            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3060                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3061            })?;
3062        let ObjectKind::Segment { .. } = &spline_object.kind else {
3063            return Err(KclError::refactor(format!(
3064                "Object is not a segment: {spline_object:?}"
3065            )));
3066        };
3067
3068        self.mutate_ast(
3069            new_ast,
3070            spline,
3071            AstMutateCommand::EditControlPointSpline {
3072                points: points_ast,
3073                construction: ctor.construction,
3074            },
3075        )?;
3076        Ok(())
3077    }
3078
3079    fn delete_segment(
3080        &mut self,
3081        new_ast: &mut ast::Node<ast::Program>,
3082        sketch: ObjectId,
3083        segment_id: ObjectId,
3084    ) -> Result<(), KclError> {
3085        // Look up existing sketch.
3086        let sketch_id = sketch;
3087        let sketch_object = self
3088            .scene_graph
3089            .objects
3090            .get(sketch_id.0)
3091            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3092        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3093            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3094        };
3095        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3096            KclError::refactor(format!(
3097                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3098            ))
3099        })?;
3100        // Look up existing segment.
3101        let segment_object =
3102            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3103                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3104            })?;
3105        let ObjectKind::Segment { .. } = &segment_object.kind else {
3106            return Err(KclError::refactor(format!(
3107                "Object is not a segment, it is {}",
3108                segment_object.kind.human_friendly_kind_with_article()
3109            )));
3110        };
3111
3112        // Modify the AST to remove the segment.
3113        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3114        Ok(())
3115    }
3116
3117    fn delete_constraint(
3118        &mut self,
3119        new_ast: &mut ast::Node<ast::Program>,
3120        sketch: ObjectId,
3121        constraint_id: ObjectId,
3122    ) -> Result<(), KclError> {
3123        // Look up existing sketch.
3124        let sketch_id = sketch;
3125        let sketch_object = self
3126            .scene_graph
3127            .objects
3128            .get(sketch_id.0)
3129            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3130        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3131            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3132        };
3133        sketch
3134            .constraints
3135            .iter()
3136            .find(|o| **o == constraint_id)
3137            .ok_or_else(|| {
3138                KclError::refactor(format!(
3139                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3140                ))
3141            })?;
3142        // Look up existing constraint.
3143        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3144            KclError::refactor(format!(
3145                "Constraint not found in scene graph: constraint={constraint_id:?}"
3146            ))
3147        })?;
3148        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3149            return Err(KclError::refactor(format!(
3150                "Object is not a constraint, it is {}",
3151                constraint_object.kind.human_friendly_kind_with_article()
3152            )));
3153        };
3154
3155        // Modify the AST to remove the constraint.
3156        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3157        Ok(())
3158    }
3159
3160    fn edit_coincident_constraint(
3161        &mut self,
3162        new_ast: &mut ast::Node<ast::Program>,
3163        constraint_id: ObjectId,
3164        segments: Vec<ConstraintSegment>,
3165    ) -> Result<(), KclError> {
3166        if segments.len() < 2 {
3167            return Err(KclError::refactor(format!(
3168                "Coincident constraint must have at least 2 inputs, got {}",
3169                segments.len()
3170            )));
3171        }
3172
3173        let segment_asts = segments
3174            .iter()
3175            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3176            .collect::<Result<Vec<_>, _>>()?;
3177
3178        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3179            elements: segment_asts,
3180            digest: None,
3181            non_code_meta: Default::default(),
3182        })));
3183
3184        self.mutate_ast(
3185            new_ast,
3186            constraint_id,
3187            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3188        )?;
3189        Ok(())
3190    }
3191
3192    fn edit_horizontal_points_constraint(
3193        &mut self,
3194        new_ast: &mut ast::Node<ast::Program>,
3195        constraint_id: ObjectId,
3196        points: Vec<ConstraintSegment>,
3197    ) -> Result<(), KclError> {
3198        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3199    }
3200
3201    fn edit_vertical_points_constraint(
3202        &mut self,
3203        new_ast: &mut ast::Node<ast::Program>,
3204        constraint_id: ObjectId,
3205        points: Vec<ConstraintSegment>,
3206    ) -> Result<(), KclError> {
3207        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3208    }
3209
3210    fn edit_axis_points_constraint(
3211        &mut self,
3212        new_ast: &mut ast::Node<ast::Program>,
3213        constraint_id: ObjectId,
3214        points: Vec<ConstraintSegment>,
3215        constraint_name: &str,
3216    ) -> Result<(), KclError> {
3217        if points.len() < 2 {
3218            return Err(KclError::refactor(format!(
3219                "{constraint_name} points constraint must have at least 2 points, got {}",
3220                points.len()
3221            )));
3222        }
3223
3224        let point_asts = points
3225            .iter()
3226            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3227            .collect::<Result<Vec<_>, _>>()?;
3228
3229        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3230            elements: point_asts,
3231            digest: None,
3232            non_code_meta: Default::default(),
3233        })));
3234
3235        self.mutate_ast(
3236            new_ast,
3237            constraint_id,
3238            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3239        )?;
3240        Ok(())
3241    }
3242
3243    /// updates the equalLength constraint with the given lines
3244    fn edit_equal_length_constraint(
3245        &mut self,
3246        new_ast: &mut ast::Node<ast::Program>,
3247        constraint_id: ObjectId,
3248        lines: Vec<ObjectId>,
3249    ) -> Result<(), KclError> {
3250        if lines.len() < 2 {
3251            return Err(KclError::refactor(format!(
3252                "Lines equal length constraint must have at least 2 lines, got {}",
3253                lines.len()
3254            )));
3255        }
3256
3257        let line_asts = lines
3258            .iter()
3259            .map(|line_id| {
3260                let line_object = self
3261                    .scene_graph
3262                    .objects
3263                    .get(line_id.0)
3264                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3265                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3266                    let kind = line_object.kind.human_friendly_kind_with_article();
3267                    return Err(KclError::refactor(format!(
3268                        "This constraint only works on Segments, but you selected {kind}"
3269                    )));
3270                };
3271                let Segment::Line(_) = line_segment else {
3272                    let kind = line_segment.human_friendly_kind_with_article();
3273                    return Err(KclError::refactor(format!(
3274                        "Only lines can be made equal length, but you selected {kind}"
3275                    )));
3276                };
3277
3278                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3279            })
3280            .collect::<Result<Vec<_>, _>>()?;
3281
3282        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3283            elements: line_asts,
3284            digest: None,
3285            non_code_meta: Default::default(),
3286        })));
3287
3288        self.mutate_ast(
3289            new_ast,
3290            constraint_id,
3291            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3292        )?;
3293        Ok(())
3294    }
3295
3296    /// Updates the parallel constraint with the given lines.
3297    fn edit_parallel_constraint(
3298        &mut self,
3299        new_ast: &mut ast::Node<ast::Program>,
3300        constraint_id: ObjectId,
3301        lines: Vec<ObjectId>,
3302    ) -> Result<(), KclError> {
3303        if lines.len() < 2 {
3304            return Err(KclError::refactor(format!(
3305                "Parallel constraint must have at least 2 lines, got {}",
3306                lines.len()
3307            )));
3308        }
3309
3310        let line_asts = lines
3311            .iter()
3312            .map(|line_id| {
3313                let line_object = self
3314                    .scene_graph
3315                    .objects
3316                    .get(line_id.0)
3317                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3318                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3319                    let kind = line_object.kind.human_friendly_kind_with_article();
3320                    return Err(KclError::refactor(format!(
3321                        "This constraint only works on Segments, but you selected {kind}"
3322                    )));
3323                };
3324                let Segment::Line(_) = line_segment else {
3325                    let kind = line_segment.human_friendly_kind_with_article();
3326                    return Err(KclError::refactor(format!(
3327                        "Only lines can be made parallel, but you selected {kind}"
3328                    )));
3329                };
3330
3331                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3332            })
3333            .collect::<Result<Vec<_>, _>>()?;
3334
3335        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3336            elements: line_asts,
3337            digest: None,
3338            non_code_meta: Default::default(),
3339        })));
3340
3341        self.mutate_ast(
3342            new_ast,
3343            constraint_id,
3344            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3345        )?;
3346        Ok(())
3347    }
3348
3349    /// Updates the equalRadius constraint with the given segments.
3350    fn edit_equal_radius_constraint(
3351        &mut self,
3352        new_ast: &mut ast::Node<ast::Program>,
3353        constraint_id: ObjectId,
3354        input: Vec<ObjectId>,
3355    ) -> Result<(), KclError> {
3356        if input.len() < 2 {
3357            return Err(KclError::refactor(format!(
3358                "equalRadius constraint must have at least 2 segments, got {}",
3359                input.len()
3360            )));
3361        }
3362
3363        let input_asts = input
3364            .iter()
3365            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3366            .collect::<Result<Vec<_>, _>>()?;
3367
3368        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3369            elements: input_asts,
3370            digest: None,
3371            non_code_meta: Default::default(),
3372        })));
3373
3374        self.mutate_ast(
3375            new_ast,
3376            constraint_id,
3377            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3378        )?;
3379        Ok(())
3380    }
3381
3382    async fn execute_after_edit(
3383        &mut self,
3384        ctx: &ExecutorContext,
3385        sketch: ObjectId,
3386        sketch_block_ref: AstNodeRef,
3387        new_ast: &mut ast::Node<ast::Program>,
3388        options: ExecuteAfterEditOptions,
3389    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3390        let ExecuteAfterEditOptions {
3391            segment_ids_edited,
3392            edit_kind,
3393            commit_solved_initial_guesses,
3394        } = options;
3395
3396        // Convert to string source to create real source ranges.
3397        let new_source = source_from_ast(new_ast);
3398        // Parse the new KCL source.
3399        let new_program = parse_frontend_mutation_source(
3400            &new_source,
3401            "Error parsing KCL source after editing",
3402            "No AST produced after editing",
3403        )?;
3404
3405        // Truncate after the sketch block for mock execution.
3406        let is_delete = edit_kind.is_delete();
3407        let truncated_program = {
3408            let mut truncated_program = new_program.clone();
3409            only_sketch_block(
3410                &mut truncated_program.ast,
3411                &sketch_block_ref,
3412                edit_kind.to_change_kind(),
3413            )
3414            .map_err(KclErrorWithOutputs::no_outputs)?;
3415            truncated_program
3416        };
3417
3418        // Execute.
3419        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3420        let mock_config = MockConfig {
3421            sketch_block_id: Some(sketch),
3422            freedom_analysis: is_delete,
3423            segment_ids_edited: segment_ids_edited.clone(),
3424            drag_anchors,
3425            ..Default::default()
3426        };
3427        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3428
3429        // Only now, after execution has succeeded, update self.program.
3430        self.program = new_program;
3431
3432        // Uses freedom_analysis: is_delete
3433        let outcome = self.update_state_after_exec(outcome, is_delete);
3434
3435        let src_delta = if commit_solved_initial_guesses {
3436            self.commit_var_solutions_to_program(&outcome, "editing")?
3437        } else {
3438            SourceDelta { text: new_source }
3439        };
3440        let scene_graph_delta = SceneGraphDelta {
3441            new_graph: self.scene_graph_for_ui(),
3442            invalidates_ids: is_delete,
3443            new_objects: Vec::new(),
3444            exec_outcome: outcome,
3445        };
3446        Ok((src_delta, scene_graph_delta))
3447    }
3448
3449    async fn execute_after_delete_sketch(
3450        &mut self,
3451        ctx: &ExecutorContext,
3452        new_ast: &mut ast::Node<ast::Program>,
3453    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3454        // Convert to string source to create real source ranges.
3455        let new_source = source_from_ast(new_ast);
3456        // Parse the new KCL source.
3457        let new_program = parse_frontend_mutation_source(
3458            &new_source,
3459            "Error parsing KCL source after editing",
3460            "No AST produced after editing",
3461        )?;
3462
3463        // Make sure to only set this if there are no errors.
3464        self.program = new_program.clone();
3465
3466        // We deleted the entire sketch block. It doesn't make sense to truncate
3467        // and execute only the sketch block. We execute the whole program with
3468        // a real engine.
3469
3470        // Execute.
3471        let outcome = ctx.run_with_caching(new_program).await?;
3472        let freedom_analysis_ran = true;
3473
3474        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3475
3476        let src_delta = SourceDelta { text: new_source };
3477        let scene_graph_delta = SceneGraphDelta {
3478            new_graph: self.scene_graph_for_ui(),
3479            invalidates_ids: true,
3480            new_objects: Vec::new(),
3481            exec_outcome: outcome,
3482        };
3483        Ok((src_delta, scene_graph_delta))
3484    }
3485
3486    /// Map a point object id into an AST reference expression for use in
3487    /// constraints. If the point is owned by a segment (line or arc), we
3488    /// reference the appropriate property on that segment (e.g. `line1.start`,
3489    /// `arc1.center`). Otherwise we reference the point directly.
3490    fn point_id_to_ast_reference(
3491        &self,
3492        point_id: ObjectId,
3493        new_ast: &mut ast::Node<ast::Program>,
3494    ) -> Result<ast::Expr, KclError> {
3495        let point_object = self
3496            .scene_graph
3497            .objects
3498            .get(point_id.0)
3499            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3500        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3501            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3502        };
3503        let Segment::Point(point) = point_segment else {
3504            return Err(KclError::refactor(format!(
3505                "Only points are currently supported: {point_object:?}"
3506            )));
3507        };
3508
3509        if let Some(owner_id) = point.owner {
3510            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3511                KclError::refactor(format!(
3512                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3513                ))
3514            })?;
3515            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3516                return Err(KclError::refactor(format!(
3517                    "Owner of point is not a segment, but found {}",
3518                    owner_object.kind.human_friendly_kind_with_article()
3519                )));
3520            };
3521
3522            match owner_segment {
3523                Segment::Line(line) => {
3524                    let property = if line.start == point_id {
3525                        LINE_PROPERTY_START
3526                    } else if line.end == point_id {
3527                        LINE_PROPERTY_END
3528                    } else {
3529                        return Err(KclError::refactor(format!(
3530                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3531                        )));
3532                    };
3533                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3534                }
3535                Segment::Arc(arc) => {
3536                    let property = if arc.start == point_id {
3537                        ARC_PROPERTY_START
3538                    } else if arc.end == point_id {
3539                        ARC_PROPERTY_END
3540                    } else if arc.center == point_id {
3541                        ARC_PROPERTY_CENTER
3542                    } else {
3543                        return Err(KclError::refactor(format!(
3544                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3545                        )));
3546                    };
3547                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3548                }
3549                Segment::Circle(circle) => {
3550                    let property = if circle.start == point_id {
3551                        CIRCLE_PROPERTY_START
3552                    } else if circle.center == point_id {
3553                        CIRCLE_PROPERTY_CENTER
3554                    } else {
3555                        return Err(KclError::refactor(format!(
3556                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3557                        )));
3558                    };
3559                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3560                }
3561                Segment::ControlPointSpline(spline) => {
3562                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3563                        return Err(KclError::refactor(format!(
3564                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3565                        )));
3566                    };
3567                    let owner_expr =
3568                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3569                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3570                    Ok(create_index_expression(controls_expr, index))
3571                }
3572                _ => Err(KclError::refactor(format!(
3573                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3574                ))),
3575            }
3576        } else {
3577            // Standalone point.
3578            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3579        }
3580    }
3581
3582    fn line_id_to_ast_reference(
3583        &self,
3584        line_id: ObjectId,
3585        new_ast: &mut ast::Node<ast::Program>,
3586    ) -> Result<ast::Expr, KclError> {
3587        let line_object = self
3588            .scene_graph
3589            .objects
3590            .get(line_id.0)
3591            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3592        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3593            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3594        };
3595        let Segment::Line(line) = line_segment else {
3596            return Err(KclError::refactor(format!(
3597                "Only lines are currently supported: {line_object:?}"
3598            )));
3599        };
3600
3601        if let Some(owner_id) = line.owner {
3602            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3603                KclError::refactor(format!(
3604                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3605                ))
3606            })?;
3607            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3608                return Err(KclError::refactor(format!(
3609                    "Owner of line is not a segment, but found {}",
3610                    owner_object.kind.human_friendly_kind_with_article()
3611                )));
3612            };
3613
3614            match owner_segment {
3615                Segment::ControlPointSpline(spline) => {
3616                    let Some(index) = spline
3617                        .controls
3618                        .windows(2)
3619                        .position(|window| window[0] == line.start && window[1] == line.end)
3620                    else {
3621                        return Err(KclError::refactor(format!(
3622                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3623                        )));
3624                    };
3625                    let owner_expr =
3626                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3627                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3628                    Ok(create_index_expression(edges_expr, index))
3629                }
3630                _ => Err(KclError::refactor(format!(
3631                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3632                ))),
3633            }
3634        } else {
3635            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3636        }
3637    }
3638
3639    fn coincident_segment_to_ast(
3640        &self,
3641        segment: &ConstraintSegment,
3642        new_ast: &mut ast::Node<ast::Program>,
3643    ) -> Result<ast::Expr, KclError> {
3644        match segment {
3645            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3646            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3647        }
3648    }
3649
3650    fn segment_id_to_constraint_ast_reference(
3651        &self,
3652        segment_id: ObjectId,
3653        new_ast: &mut ast::Node<ast::Program>,
3654    ) -> Result<ast::Expr, KclError> {
3655        let segment_object = self
3656            .scene_graph
3657            .objects
3658            .get(segment_id.0)
3659            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3660        let ObjectKind::Segment { segment } = &segment_object.kind else {
3661            return Err(KclError::refactor(format!(
3662                "Object is not a segment, it is {}",
3663                segment_object.kind.human_friendly_kind_with_article()
3664            )));
3665        };
3666
3667        match segment {
3668            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3669            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3670            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3671            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3672            Segment::ControlPointSpline(_) => {
3673                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3674            }
3675        }
3676    }
3677
3678    fn axis_constraint_segment_to_ast(
3679        &self,
3680        segment: &ConstraintSegment,
3681        new_ast: &mut ast::Node<ast::Program>,
3682    ) -> Result<ast::Expr, KclError> {
3683        match segment {
3684            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3685            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3686        }
3687    }
3688
3689    async fn add_coincident(
3690        &mut self,
3691        sketch: ObjectId,
3692        coincident: Coincident,
3693        new_ast: &mut ast::Node<ast::Program>,
3694    ) -> Result<AstNodeRef, KclError> {
3695        let sketch_id = sketch;
3696        for segment in &coincident.segments {
3697            let ConstraintSegment::Segment(segment_id) = segment else {
3698                continue;
3699            };
3700            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3701                continue;
3702            };
3703            if matches!(
3704                segment_object.kind,
3705                ObjectKind::Segment {
3706                    segment: Segment::ControlPointSpline(_)
3707                }
3708            ) {
3709                return Err(KclError::refactor(
3710                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3711                        .to_owned(),
3712                ));
3713            }
3714        }
3715        let segment_asts = coincident
3716            .segments
3717            .iter()
3718            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3719            .collect::<Result<Vec<_>, _>>()?;
3720        if segment_asts.len() < 2 {
3721            return Err(KclError::refactor(format!(
3722                "Coincident constraint must have at least 2 inputs, got {}",
3723                segment_asts.len()
3724            )));
3725        }
3726
3727        // Create the coincident() call using shared helper.
3728        let coincident_ast = create_coincident_ast(segment_asts);
3729
3730        // Add the line to the AST of the sketch block.
3731        let (sketch_block_ref, _) = self.mutate_ast(
3732            new_ast,
3733            sketch_id,
3734            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3735        )?;
3736        Ok(sketch_block_ref)
3737    }
3738
3739    async fn add_distance(
3740        &mut self,
3741        sketch: ObjectId,
3742        distance: Distance,
3743        new_ast: &mut ast::Node<ast::Program>,
3744    ) -> Result<AstNodeRef, KclError> {
3745        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3746    }
3747
3748    fn distance_constraint_ast_parts(
3749        &self,
3750        function_name: &str,
3751        distance: &Distance,
3752        new_ast: &mut ast::Node<ast::Program>,
3753    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3754        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3755            [pt0, pt1] => [
3756                self.coincident_segment_to_ast(pt0, new_ast)?,
3757                self.coincident_segment_to_ast(pt1, new_ast)?,
3758            ],
3759            _ => {
3760                return Err(KclError::refactor(format!(
3761                    "Distance constraint must have exactly 2 segments, got {}",
3762                    distance.segments.len()
3763                )));
3764            }
3765        };
3766
3767        let arguments = match &distance.label_position {
3768            Some(label_position) => vec![ast::LabeledArg {
3769                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3770                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3771            }],
3772            None => Default::default(),
3773        };
3774
3775        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3776            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3777            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3778                ast::ArrayExpression {
3779                    elements: vec![segment0_ast, segment1_ast],
3780                    digest: None,
3781                    non_code_meta: Default::default(),
3782                },
3783            )))),
3784            arguments,
3785            digest: None,
3786            non_code_meta: Default::default(),
3787        })));
3788        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3789            value: ast::LiteralValue::Number {
3790                value: distance.distance.value,
3791                suffix: distance.distance.units,
3792            },
3793            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3794                KclError::refactor(format!(
3795                    "Could not format numeric suffix: {:?}",
3796                    distance.distance.units
3797                ))
3798            })?,
3799            digest: None,
3800        })));
3801
3802        Ok((call, value))
3803    }
3804
3805    fn add_distance_constraint(
3806        &mut self,
3807        sketch: ObjectId,
3808        function_name: &str,
3809        distance: Distance,
3810        new_ast: &mut ast::Node<ast::Program>,
3811    ) -> Result<AstNodeRef, KclError> {
3812        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3813        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3814            left: call,
3815            operator: ast::BinaryOperator::Eq,
3816            right: value,
3817            digest: None,
3818        })));
3819
3820        let (sketch_block_ref, _) = self.mutate_ast(
3821            new_ast,
3822            sketch,
3823            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3824        )?;
3825        Ok(sketch_block_ref)
3826    }
3827
3828    async fn add_angle(
3829        &mut self,
3830        sketch: ObjectId,
3831        angle: Angle,
3832        new_ast: &mut ast::Node<ast::Program>,
3833    ) -> Result<AstNodeRef, KclError> {
3834        let sketch_id = sketch;
3835        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3836        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3837            left: angle_call_ast,
3838            operator: ast::BinaryOperator::Eq,
3839            right: angle_value_ast,
3840            digest: None,
3841        })));
3842
3843        // Add the line to the AST of the sketch block.
3844        let (sketch_block_ref, _) = self.mutate_ast(
3845            new_ast,
3846            sketch_id,
3847            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3848        )?;
3849        Ok(sketch_block_ref)
3850    }
3851
3852    fn angle_constraint_ast_parts(
3853        &self,
3854        angle: &Angle,
3855        new_ast: &mut ast::Node<ast::Program>,
3856    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3857        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3858            return Err(KclError::refactor(format!(
3859                "Angle constraint must have exactly 2 lines, got {}",
3860                angle.lines.len()
3861            )));
3862        };
3863
3864        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3865        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3866        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3867            elements: vec![l0_ast, l1_ast],
3868            digest: None,
3869            non_code_meta: Default::default(),
3870        })));
3871
3872        if angle.inverse == Some(true) && angle.sector.is_none() {
3873            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3874        }
3875
3876        let uses_angle_dimension = angle.sector.is_some();
3877        let mut arguments = if uses_angle_dimension {
3878            vec![ast::LabeledArg {
3879                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3880                arg: lines_ast.clone(),
3881            }]
3882        } else {
3883            Default::default()
3884        };
3885
3886        if let Some(sector) = angle.sector {
3887            arguments.push(ast::LabeledArg {
3888                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3889                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3890                    value: ast::LiteralValue::Number {
3891                        value: f64::from(sector),
3892                        suffix: NumericSuffix::None,
3893                    },
3894                    raw: sector.to_string(),
3895                    digest: None,
3896                }))),
3897            });
3898        }
3899
3900        if angle.inverse == Some(true) {
3901            arguments.push(ast::LabeledArg {
3902                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3903                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3904                    value: ast::LiteralValue::Bool(true),
3905                    raw: true.to_string(),
3906                    digest: None,
3907                }))),
3908            });
3909        }
3910
3911        if let Some(label_position) = &angle.label_position {
3912            arguments.push(ast::LabeledArg {
3913                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3914                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3915            });
3916        }
3917
3918        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3919            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3920                ANGLE_DIMENSION_FN
3921            } else {
3922                ANGLE_FN
3923            })),
3924            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3925            arguments,
3926            digest: None,
3927            non_code_meta: Default::default(),
3928        })));
3929        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3930            value: ast::LiteralValue::Number {
3931                value: angle.angle.value,
3932                suffix: angle.angle.units,
3933            },
3934            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3935                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3936            digest: None,
3937        })));
3938
3939        Ok((call, value))
3940    }
3941
3942    async fn add_tangent(
3943        &mut self,
3944        sketch: ObjectId,
3945        tangent: Tangent,
3946        new_ast: &mut ast::Node<ast::Program>,
3947    ) -> Result<AstNodeRef, KclError> {
3948        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3949            return Err(KclError::refactor(format!(
3950                "Tangent constraint must have exactly 2 segments, got {}",
3951                tangent.input.len()
3952            )));
3953        };
3954        let sketch_id = sketch;
3955
3956        let seg0_object = self
3957            .scene_graph
3958            .objects
3959            .get(seg0_id.0)
3960            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3961        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3962            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3963        };
3964        let seg0_ast = match seg0_segment {
3965            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3966                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3967            }
3968            _ => {
3969                return Err(KclError::refactor(format!(
3970                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3971                )));
3972            }
3973        };
3974
3975        let seg1_object = self
3976            .scene_graph
3977            .objects
3978            .get(seg1_id.0)
3979            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3980        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3981            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3982        };
3983        let seg1_ast = match seg1_segment {
3984            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3985                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3986            }
3987            _ => {
3988                return Err(KclError::refactor(format!(
3989                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3990                )));
3991            }
3992        };
3993
3994        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3995        let (sketch_block_ref, _) = self.mutate_ast(
3996            new_ast,
3997            sketch_id,
3998            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3999        )?;
4000        Ok(sketch_block_ref)
4001    }
4002
4003    async fn add_symmetric(
4004        &mut self,
4005        sketch: ObjectId,
4006        symmetric: Symmetric,
4007        new_ast: &mut ast::Node<ast::Program>,
4008    ) -> Result<AstNodeRef, KclError> {
4009        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4010            return Err(KclError::refactor(format!(
4011                "Symmetric constraint must have exactly 2 inputs, got {}",
4012                symmetric.input.len()
4013            )));
4014        };
4015        let sketch_id = sketch;
4016
4017        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4018        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4019        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4020
4021        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4022        let (sketch_block_ref, _) = self.mutate_ast(
4023            new_ast,
4024            sketch_id,
4025            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4026        )?;
4027        Ok(sketch_block_ref)
4028    }
4029
4030    async fn add_midpoint(
4031        &mut self,
4032        sketch: ObjectId,
4033        midpoint: Midpoint,
4034        new_ast: &mut ast::Node<ast::Program>,
4035    ) -> Result<AstNodeRef, KclError> {
4036        let sketch_id = sketch;
4037        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4038
4039        let segment_object = self
4040            .scene_graph
4041            .objects
4042            .get(midpoint.segment.0)
4043            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4044        let ObjectKind::Segment {
4045            segment: midpoint_segment,
4046        } = &segment_object.kind
4047        else {
4048            return Err(KclError::refactor(format!(
4049                "Object must be a segment, but it was {}",
4050                segment_object.kind.human_friendly_kind_with_article()
4051            )));
4052        };
4053        let segment_ast = match midpoint_segment {
4054            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4055            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4056            _ => {
4057                return Err(KclError::refactor(format!(
4058                    "Midpoint target must be a line or arc segment but it was {}",
4059                    midpoint_segment.human_friendly_kind_with_article()
4060                )));
4061            }
4062        };
4063
4064        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4065        let (sketch_block_ref, _) = self.mutate_ast(
4066            new_ast,
4067            sketch_id,
4068            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4069        )?;
4070        Ok(sketch_block_ref)
4071    }
4072
4073    async fn add_equal_radius(
4074        &mut self,
4075        sketch: ObjectId,
4076        equal_radius: EqualRadius,
4077        new_ast: &mut ast::Node<ast::Program>,
4078    ) -> Result<AstNodeRef, KclError> {
4079        if equal_radius.input.len() < 2 {
4080            return Err(KclError::refactor(format!(
4081                "equalRadius constraint must have at least 2 segments, got {}",
4082                equal_radius.input.len()
4083            )));
4084        }
4085
4086        let sketch_id = sketch;
4087        let input_asts = equal_radius
4088            .input
4089            .iter()
4090            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4091            .collect::<Result<Vec<_>, _>>()?;
4092
4093        let equal_radius_ast = create_equal_radius_ast(input_asts);
4094        let (sketch_block_ref, _) = self.mutate_ast(
4095            new_ast,
4096            sketch_id,
4097            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4098        )?;
4099        Ok(sketch_block_ref)
4100    }
4101
4102    async fn add_radius(
4103        &mut self,
4104        sketch: ObjectId,
4105        radius: Radius,
4106        new_ast: &mut ast::Node<ast::Program>,
4107    ) -> Result<AstNodeRef, KclError> {
4108        let params = ArcSizeConstraintParams {
4109            points: vec![radius.arc],
4110            function_name: RADIUS_FN,
4111            value: radius.radius.value,
4112            units: radius.radius.units,
4113            label_position: radius.label_position,
4114            constraint_type_name: "Radius",
4115        };
4116        self.add_arc_size_constraint(sketch, params, new_ast).await
4117    }
4118
4119    async fn add_diameter(
4120        &mut self,
4121        sketch: ObjectId,
4122        diameter: Diameter,
4123        new_ast: &mut ast::Node<ast::Program>,
4124    ) -> Result<AstNodeRef, KclError> {
4125        let params = ArcSizeConstraintParams {
4126            points: vec![diameter.arc],
4127            function_name: DIAMETER_FN,
4128            value: diameter.diameter.value,
4129            units: diameter.diameter.units,
4130            label_position: diameter.label_position,
4131            constraint_type_name: "Diameter",
4132        };
4133        self.add_arc_size_constraint(sketch, params, new_ast).await
4134    }
4135
4136    async fn add_fixed_constraints(
4137        &mut self,
4138        sketch: ObjectId,
4139        points: Vec<FixedPoint>,
4140        new_ast: &mut ast::Node<ast::Program>,
4141    ) -> Result<AstNodeRef, KclError> {
4142        let mut sketch_block_ref = None;
4143
4144        for fixed_point in points {
4145            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4146            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4147                .map_err(|err| KclError::refactor(err.to_string()))?;
4148
4149            let (sketch_ref, _) = self.mutate_ast(
4150                new_ast,
4151                sketch,
4152                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4153            )?;
4154            sketch_block_ref = Some(sketch_ref);
4155        }
4156
4157        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4158    }
4159
4160    async fn add_arc_size_constraint(
4161        &mut self,
4162        sketch: ObjectId,
4163        params: ArcSizeConstraintParams,
4164        new_ast: &mut ast::Node<ast::Program>,
4165    ) -> Result<AstNodeRef, KclError> {
4166        let sketch_id = sketch;
4167
4168        // Constraint must have exactly 1 argument (arc segment)
4169        if params.points.len() != 1 {
4170            return Err(KclError::refactor(format!(
4171                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4172                params.constraint_type_name,
4173                params.points.len()
4174            )));
4175        }
4176
4177        let arc_id = params.points[0];
4178        let arc_object = self
4179            .scene_graph
4180            .objects
4181            .get(arc_id.0)
4182            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4183        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4184            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4185        };
4186        let ref_type = match arc_segment {
4187            Segment::Arc(_) => ARC_VARIABLE,
4188            Segment::Circle(_) => CIRCLE_VARIABLE,
4189            _ => {
4190                return Err(KclError::refactor(format!(
4191                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4192                    params.constraint_type_name
4193                )));
4194            }
4195        };
4196        // Reference the arc/circle segment directly
4197        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4198        let arguments = match &params.label_position {
4199            Some(label_position) => vec![ast::LabeledArg {
4200                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4201                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4202            }],
4203            None => Default::default(),
4204        };
4205
4206        // Create the function call.
4207        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4208            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4209            unlabeled: Some(arc_ast),
4210            arguments,
4211            digest: None,
4212            non_code_meta: Default::default(),
4213        })));
4214        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4215            left: call_ast,
4216            operator: ast::BinaryOperator::Eq,
4217            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4218                value: ast::LiteralValue::Number {
4219                    value: params.value,
4220                    suffix: params.units,
4221                },
4222                raw: format_number_literal(params.value, params.units, None)
4223                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4224                digest: None,
4225            }))),
4226            digest: None,
4227        })));
4228
4229        // Add the line to the AST of the sketch block.
4230        let (sketch_block_ref, _) = self.mutate_ast(
4231            new_ast,
4232            sketch_id,
4233            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4234        )?;
4235        Ok(sketch_block_ref)
4236    }
4237
4238    async fn add_horizontal_distance(
4239        &mut self,
4240        sketch: ObjectId,
4241        distance: Distance,
4242        new_ast: &mut ast::Node<ast::Program>,
4243    ) -> Result<AstNodeRef, KclError> {
4244        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4245    }
4246
4247    async fn add_vertical_distance(
4248        &mut self,
4249        sketch: ObjectId,
4250        distance: Distance,
4251        new_ast: &mut ast::Node<ast::Program>,
4252    ) -> Result<AstNodeRef, KclError> {
4253        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4254    }
4255
4256    async fn add_horizontal(
4257        &mut self,
4258        sketch: ObjectId,
4259        horizontal: Horizontal,
4260        new_ast: &mut ast::Node<ast::Program>,
4261    ) -> Result<AstNodeRef, KclError> {
4262        let sketch_id = sketch;
4263
4264        // Map the runtime objects back to variable names.
4265        let first_arg_ast = match horizontal {
4266            Horizontal::Line { line } => {
4267                let line_object = self
4268                    .scene_graph
4269                    .objects
4270                    .get(line.0)
4271                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4272                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4273                    let kind = line_object.kind.human_friendly_kind_with_article();
4274                    return Err(KclError::refactor(format!(
4275                        "This constraint only works on Segments, but you selected {kind}"
4276                    )));
4277                };
4278                let Segment::Line(_) = line_segment else {
4279                    return Err(KclError::refactor(format!(
4280                        "Only lines can be made horizontal, but you selected {}",
4281                        line_segment.human_friendly_kind_with_article(),
4282                    )));
4283                };
4284                self.line_id_to_ast_reference(line, new_ast)?
4285            }
4286            Horizontal::Points { points } => {
4287                let point_asts = points
4288                    .iter()
4289                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4290                    .collect::<Result<Vec<_>, _>>()?;
4291                ast::ArrayExpression::new(point_asts).into()
4292            }
4293        };
4294        // Create the horizontal() call using shared helper.
4295        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4296
4297        // Add the line to the AST of the sketch block.
4298        let (sketch_block_ref, _) = self.mutate_ast(
4299            new_ast,
4300            sketch_id,
4301            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4302        )?;
4303        Ok(sketch_block_ref)
4304    }
4305
4306    async fn add_lines_equal_length(
4307        &mut self,
4308        sketch: ObjectId,
4309        lines_equal_length: LinesEqualLength,
4310        new_ast: &mut ast::Node<ast::Program>,
4311    ) -> Result<AstNodeRef, KclError> {
4312        if lines_equal_length.lines.len() < 2 {
4313            return Err(KclError::refactor(format!(
4314                "Lines equal length constraint must have at least 2 lines, got {}",
4315                lines_equal_length.lines.len()
4316            )));
4317        };
4318
4319        let sketch_id = sketch;
4320
4321        // Map the runtime objects back to variable names.
4322        let line_asts = lines_equal_length
4323            .lines
4324            .iter()
4325            .map(|line_id| {
4326                let line_object = self
4327                    .scene_graph
4328                    .objects
4329                    .get(line_id.0)
4330                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332                    let kind = line_object.kind.human_friendly_kind_with_article();
4333                    return Err(KclError::refactor(format!(
4334                        "This constraint only works on Segments, but you selected {kind}"
4335                    )));
4336                };
4337                let Segment::Line(_) = line_segment else {
4338                    let kind = line_segment.human_friendly_kind_with_article();
4339                    return Err(KclError::refactor(format!(
4340                        "Only lines can be made equal length, but you selected {kind}"
4341                    )));
4342                };
4343
4344                self.line_id_to_ast_reference(*line_id, new_ast)
4345            })
4346            .collect::<Result<Vec<_>, _>>()?;
4347
4348        // Create the equalLength() call using shared helper.
4349        let equal_length_ast = create_equal_length_ast(line_asts);
4350
4351        // Add the constraint to the AST of the sketch block.
4352        let (sketch_block_ref, _) = self.mutate_ast(
4353            new_ast,
4354            sketch_id,
4355            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4356        )?;
4357        Ok(sketch_block_ref)
4358    }
4359
4360    fn equal_radius_segment_id_to_ast_reference(
4361        &mut self,
4362        segment_id: ObjectId,
4363        new_ast: &mut ast::Node<ast::Program>,
4364    ) -> Result<ast::Expr, KclError> {
4365        let segment_object = self
4366            .scene_graph
4367            .objects
4368            .get(segment_id.0)
4369            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4370        let ObjectKind::Segment { segment } = &segment_object.kind else {
4371            return Err(KclError::refactor(format!(
4372                "Object is not a segment, it was {}",
4373                segment_object.kind.human_friendly_kind_with_article()
4374            )));
4375        };
4376
4377        let ref_type = match segment {
4378            Segment::Arc(_) => ARC_VARIABLE,
4379            Segment::Circle(_) => CIRCLE_VARIABLE,
4380            _ => {
4381                return Err(KclError::refactor(format!(
4382                    "equalRadius supports only arc/circle segments, got {}",
4383                    segment.human_friendly_kind_with_article()
4384                )));
4385            }
4386        };
4387
4388        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4389    }
4390
4391    fn symmetric_input_id_to_ast_reference(
4392        &mut self,
4393        segment_id: ObjectId,
4394        new_ast: &mut ast::Node<ast::Program>,
4395    ) -> Result<ast::Expr, KclError> {
4396        let segment_object = self
4397            .scene_graph
4398            .objects
4399            .get(segment_id.0)
4400            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4401        let ObjectKind::Segment { segment } = &segment_object.kind else {
4402            return Err(KclError::refactor(format!(
4403                "Object is not a segment, it was {}",
4404                segment_object.kind.human_friendly_kind_with_article()
4405            )));
4406        };
4407
4408        match segment {
4409            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4410            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4411            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4412            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4413            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4414                "Symmetric does not yet support control point splines".to_owned(),
4415            )),
4416        }
4417    }
4418
4419    fn symmetric_axis_id_to_ast_reference(
4420        &mut self,
4421        segment_id: ObjectId,
4422        new_ast: &mut ast::Node<ast::Program>,
4423    ) -> Result<ast::Expr, KclError> {
4424        let segment_object = self
4425            .scene_graph
4426            .objects
4427            .get(segment_id.0)
4428            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4429        let ObjectKind::Segment { segment } = &segment_object.kind else {
4430            return Err(KclError::refactor(format!(
4431                "Object is not a segment, it was {}",
4432                segment_object.kind.human_friendly_kind_with_article()
4433            )));
4434        };
4435        match segment {
4436            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4437            _ => Err(KclError::refactor(format!(
4438                "Symmetric axis must be a line, got {}",
4439                segment.human_friendly_kind_with_article()
4440            ))),
4441        }
4442    }
4443
4444    async fn add_parallel(
4445        &mut self,
4446        sketch: ObjectId,
4447        parallel: Parallel,
4448        new_ast: &mut ast::Node<ast::Program>,
4449    ) -> Result<AstNodeRef, KclError> {
4450        if parallel.lines.len() < 2 {
4451            return Err(KclError::refactor(format!(
4452                "Parallel constraint must have at least 2 lines, got {}",
4453                parallel.lines.len()
4454            )));
4455        };
4456
4457        let sketch_id = sketch;
4458
4459        let line_asts = parallel
4460            .lines
4461            .iter()
4462            .map(|line_id| {
4463                let line_object = self
4464                    .scene_graph
4465                    .objects
4466                    .get(line_id.0)
4467                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4468                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4469                    let kind = line_object.kind.human_friendly_kind_with_article();
4470                    return Err(KclError::refactor(format!(
4471                        "This constraint only works on Segments, but you selected {kind}"
4472                    )));
4473                };
4474                let Segment::Line(_) = line_segment else {
4475                    let kind = line_segment.human_friendly_kind_with_article();
4476                    return Err(KclError::refactor(format!(
4477                        "Only lines can be made parallel, but you selected {kind}"
4478                    )));
4479                };
4480
4481                self.line_id_to_ast_reference(*line_id, new_ast)
4482            })
4483            .collect::<Result<Vec<_>, _>>()?;
4484
4485        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4486            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4487            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4488                ast::ArrayExpression {
4489                    elements: line_asts,
4490                    digest: None,
4491                    non_code_meta: Default::default(),
4492                },
4493            )))),
4494            arguments: Default::default(),
4495            digest: None,
4496            non_code_meta: Default::default(),
4497        })));
4498
4499        let (sketch_block_ref, _) = self.mutate_ast(
4500            new_ast,
4501            sketch_id,
4502            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4503        )?;
4504        Ok(sketch_block_ref)
4505    }
4506
4507    async fn add_perpendicular(
4508        &mut self,
4509        sketch: ObjectId,
4510        perpendicular: Perpendicular,
4511        new_ast: &mut ast::Node<ast::Program>,
4512    ) -> Result<AstNodeRef, KclError> {
4513        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4514            .await
4515    }
4516
4517    async fn add_lines_at_angle_constraint(
4518        &mut self,
4519        sketch: ObjectId,
4520        angle_kind: LinesAtAngleKind,
4521        lines: Vec<ObjectId>,
4522        new_ast: &mut ast::Node<ast::Program>,
4523    ) -> Result<AstNodeRef, KclError> {
4524        let &[line0_id, line1_id] = lines.as_slice() else {
4525            return Err(KclError::refactor(format!(
4526                "{} constraint must have exactly 2 lines, got {}",
4527                angle_kind.to_function_name(),
4528                lines.len()
4529            )));
4530        };
4531
4532        let sketch_id = sketch;
4533
4534        // Map the runtime objects back to variable names.
4535        let line0_object = self
4536            .scene_graph
4537            .objects
4538            .get(line0_id.0)
4539            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4540        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4541            let kind = line0_object.kind.human_friendly_kind_with_article();
4542            return Err(KclError::refactor(format!(
4543                "This constraint only works on Segments, but you selected {kind}"
4544            )));
4545        };
4546        let Segment::Line(_) = line0_segment else {
4547            return Err(KclError::refactor(format!(
4548                "Only lines can be made {}, but you selected {}",
4549                angle_kind.to_function_name(),
4550                line0_segment.human_friendly_kind_with_article(),
4551            )));
4552        };
4553        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4554
4555        let line1_object = self
4556            .scene_graph
4557            .objects
4558            .get(line1_id.0)
4559            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4560        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4561            let kind = line1_object.kind.human_friendly_kind_with_article();
4562            return Err(KclError::refactor(format!(
4563                "This constraint only works on Segments, but you selected {kind}"
4564            )));
4565        };
4566        let Segment::Line(_) = line1_segment else {
4567            return Err(KclError::refactor(format!(
4568                "Only lines can be made {}, but you selected {}",
4569                angle_kind.to_function_name(),
4570                line1_segment.human_friendly_kind_with_article(),
4571            )));
4572        };
4573        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4574
4575        // Create the parallel() or perpendicular() call.
4576        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4577            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4578            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4579                ast::ArrayExpression {
4580                    elements: vec![line0_ast, line1_ast],
4581                    digest: None,
4582                    non_code_meta: Default::default(),
4583                },
4584            )))),
4585            arguments: Default::default(),
4586            digest: None,
4587            non_code_meta: Default::default(),
4588        })));
4589
4590        // Add the constraint to the AST of the sketch block.
4591        let (sketch_block_ref, _) = self.mutate_ast(
4592            new_ast,
4593            sketch_id,
4594            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4595        )?;
4596        Ok(sketch_block_ref)
4597    }
4598
4599    async fn add_vertical(
4600        &mut self,
4601        sketch: ObjectId,
4602        vertical: Vertical,
4603        new_ast: &mut ast::Node<ast::Program>,
4604    ) -> Result<AstNodeRef, KclError> {
4605        let sketch_id = sketch;
4606
4607        let first_arg_ast = match vertical {
4608            Vertical::Line { line } => {
4609                // Map the runtime objects back to variable names.
4610                let line_object = self
4611                    .scene_graph
4612                    .objects
4613                    .get(line.0)
4614                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4615                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4616                    let kind = line_object.kind.human_friendly_kind_with_article();
4617                    return Err(KclError::refactor(format!(
4618                        "This constraint only works on Segments, but you selected {kind}"
4619                    )));
4620                };
4621                let Segment::Line(_) = line_segment else {
4622                    return Err(KclError::refactor(format!(
4623                        "Only lines can be made vertical, but you selected {}",
4624                        line_segment.human_friendly_kind_with_article()
4625                    )));
4626                };
4627                self.line_id_to_ast_reference(line, new_ast)?
4628            }
4629            Vertical::Points { points } => {
4630                let point_asts = points
4631                    .iter()
4632                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4633                    .collect::<Result<Vec<_>, _>>()?;
4634                ast::ArrayExpression::new(point_asts).into()
4635            }
4636        };
4637        // Create the vertical() call using shared helper.
4638        let vertical_ast = create_vertical_ast(first_arg_ast);
4639
4640        // Add the line to the AST of the sketch block.
4641        let (sketch_block_ref, _) = self.mutate_ast(
4642            new_ast,
4643            sketch_id,
4644            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4645        )?;
4646        Ok(sketch_block_ref)
4647    }
4648
4649    async fn execute_after_add_constraint(
4650        &mut self,
4651        ctx: &ExecutorContext,
4652        sketch_id: ObjectId,
4653        sketch_block_ref: AstNodeRef,
4654        new_ast: &mut ast::Node<ast::Program>,
4655    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4656        // Convert to string source to create real source ranges.
4657        let new_source = source_from_ast(new_ast);
4658        // Parse the new KCL source.
4659        let new_program = parse_frontend_mutation_source(
4660            &new_source,
4661            "Error parsing KCL source after adding constraint",
4662            "No AST produced after adding constraint",
4663        )?;
4664        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4665            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4666                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4667            )))
4668        })?;
4669
4670        // Truncate after the sketch block for mock execution.
4671        // Use a clone so we don't mutate new_program yet
4672        let mut truncated_program = new_program.clone();
4673        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4674            .map_err(KclErrorWithOutputs::no_outputs)?;
4675
4676        // Execute - if this fails, we haven't modified self yet, so state is safe
4677        let outcome = ctx
4678            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4679            .await?;
4680
4681        let new_object_ids = {
4682            // Extract the constraint ID from the execution outcome using source_range_to_object
4683            let constraint_id = outcome
4684                .source_range_to_object
4685                .get(&constraint_node_ref.range)
4686                .copied()
4687                .ok_or_else(|| {
4688                    KclErrorWithOutputs::from_error_outcome(
4689                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4690                        outcome.clone(),
4691                    )
4692                })?;
4693            vec![constraint_id]
4694        };
4695
4696        // Only now, after all operations succeeded, update self.program.
4697        // This ensures state is only modified if everything succeeds.
4698        self.program = new_program;
4699
4700        // Uses MockConfig::default() which has freedom_analysis: true
4701        let outcome = self.update_state_after_exec(outcome, true);
4702
4703        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4704        let scene_graph_delta = SceneGraphDelta {
4705            new_graph: self.scene_graph_for_ui(),
4706            invalidates_ids: false,
4707            new_objects: new_object_ids,
4708            exec_outcome: outcome,
4709        };
4710        Ok((src_delta, scene_graph_delta))
4711    }
4712
4713    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4714        let commit_failure = || {
4715            KclErrorWithOutputs::from_error_outcome(
4716                KclError::refactor(format!("Could not update KCL after {operation}.")),
4717                outcome.clone(),
4718            )
4719        };
4720
4721        let default_length_unit = self.default_length_unit();
4722        let mut settled_ast = self.program.ast.clone();
4723        let mut committed_solver_value = false;
4724        for (var_range, node_path, value) in &outcome.var_solutions {
4725            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4726                return Err(commit_failure());
4727            };
4728            let new_value = match &lookup {
4729                Some(current_literal) => {
4730                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4731                        continue;
4732                    }
4733                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4734                }
4735                None => {
4736                    // Bare `var` with no initial literal to compare against;
4737                    // always commit, using the module's default length unit as
4738                    // an explicit suffix so the written value carries units.
4739                    Number {
4740                        value: number_value_in_default_length_units(*value, default_length_unit),
4741                        units: default_length_unit.into(),
4742                    }
4743                }
4744            };
4745            committed_solver_value = true;
4746            let source_ref = SourceRef::Simple {
4747                range: *var_range,
4748                node_path: node_path.clone(),
4749            };
4750            mutate_ast_node_by_source_ref(
4751                &mut settled_ast,
4752                &source_ref,
4753                AstMutateCommand::EditVarInitialValue { value: new_value },
4754            )
4755            .map_err(|_| commit_failure())?;
4756        }
4757
4758        if !committed_solver_value {
4759            return Ok(SourceDelta {
4760                text: self.program.original_file_contents.clone(),
4761            });
4762        }
4763
4764        let settled_source = source_from_ast(&settled_ast);
4765        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4766        if !errors.is_empty() {
4767            return Err(commit_failure());
4768        }
4769        let Some(settled_program) = settled_program else {
4770            return Err(commit_failure());
4771        };
4772
4773        self.program = settled_program;
4774
4775        Ok(SourceDelta { text: settled_source })
4776    }
4777
4778    // Find constraints that reference the given segments.
4779    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4780        if segment_ids_set.contains(&segment_id) {
4781            return true;
4782        }
4783
4784        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4785            return false;
4786        };
4787        let ObjectKind::Segment { segment } = &segment_object.kind else {
4788            return false;
4789        };
4790        let Segment::Point(point) = segment else {
4791            return false;
4792        };
4793
4794        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4795    }
4796
4797    fn remaining_constraint_segments(
4798        &self,
4799        segments: &[ConstraintSegment],
4800        segment_ids_set: &AhashIndexSet<ObjectId>,
4801    ) -> Vec<ConstraintSegment> {
4802        segments
4803            .iter()
4804            .copied()
4805            .filter(|segment| match segment {
4806                ConstraintSegment::Origin(_) => true,
4807                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4808            })
4809            .collect()
4810    }
4811
4812    fn find_referenced_constraints(
4813        &self,
4814        sketch_id: ObjectId,
4815        segment_ids_set: &AhashIndexSet<ObjectId>,
4816    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4817        // Look up the sketch.
4818        let sketch_object = self
4819            .scene_graph
4820            .objects
4821            .get(sketch_id.0)
4822            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4823        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4824            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4825        };
4826        let segment_or_owner_matches = |segment_id: ObjectId| {
4827            if segment_ids_set.contains(&segment_id) {
4828                return true;
4829            }
4830            let segment_object = self.scene_graph.objects.get(segment_id.0);
4831            if let Some(obj) = segment_object
4832                && let ObjectKind::Segment { segment } = &obj.kind
4833            {
4834                match segment {
4835                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4836                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4837                    _ => false,
4838                }
4839            } else {
4840                false
4841            }
4842        };
4843        let mut constraint_ids_set = AhashIndexSet::default();
4844        for constraint_id in &sketch.constraints {
4845            let constraint_object = self
4846                .scene_graph
4847                .objects
4848                .get(constraint_id.0)
4849                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4850            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4851                return Err(KclError::refactor(format!(
4852                    "Object is not a constraint, it is {}",
4853                    constraint_object.kind.human_friendly_kind_with_article()
4854                )));
4855            };
4856            let depends_on_segment = match constraint {
4857                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4858                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4859                Constraint::Fixed(fixed) => fixed
4860                    .points
4861                    .iter()
4862                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4863                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4864                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4865                Constraint::EqualRadius(equal_radius) => {
4866                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4867                }
4868                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4869                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4870                Constraint::Horizontal(h) => match h {
4871                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4872                    Horizontal::Points { points } => points.iter().any(|point| match point {
4873                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4874                        ConstraintSegment::Origin(_) => false,
4875                    }),
4876                },
4877                Constraint::Vertical(v) => match v {
4878                    Vertical::Line { line } => segment_or_owner_matches(*line),
4879                    Vertical::Points { points } => points.iter().any(|point| match point {
4880                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4881                        ConstraintSegment::Origin(_) => false,
4882                    }),
4883                },
4884                Constraint::LinesEqualLength(lines_equal_length) => {
4885                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4886                }
4887                Constraint::Midpoint(midpoint) => {
4888                    segment_or_owner_matches(midpoint.segment)
4889                        || matches!(
4890                            midpoint.point,
4891                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4892                        )
4893                }
4894                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4895                Constraint::Perpendicular(perpendicular) => {
4896                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4897                }
4898                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4899                Constraint::Symmetric(symmetric) => {
4900                    segment_or_owner_matches(symmetric.axis)
4901                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4902                }
4903                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4904            };
4905            if depends_on_segment {
4906                constraint_ids_set.insert(*constraint_id);
4907            }
4908        }
4909        Ok(constraint_ids_set)
4910    }
4911
4912    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4913        let mut outcome = outcome;
4914        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4915        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4916
4917        if freedom_analysis_ran {
4918            // When freedom analysis ran, replace the cache entirely with new values
4919            // Don't merge with old values since IDs might have changed
4920            self.point_freedom_cache.clear();
4921            for new_obj in &new_objects {
4922                if let ObjectKind::Segment {
4923                    segment: crate::front::Segment::Point(point),
4924                } = &new_obj.kind
4925                {
4926                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4927                }
4928            }
4929            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4930            // Objects are already correct from the analysis, just use them as-is
4931            self.scene_graph.objects = new_objects;
4932        } else {
4933            // When freedom analysis didn't run, preserve old values and merge
4934            // Before replacing objects, extract and store freedom values from old objects
4935            for old_obj in &self.scene_graph.objects {
4936                if let ObjectKind::Segment {
4937                    segment: crate::front::Segment::Point(point),
4938                } = &old_obj.kind
4939                {
4940                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4941                }
4942            }
4943
4944            // Update objects, preserving stored freedom values when new is Free (might be default)
4945            let mut updated_objects = Vec::with_capacity(new_objects.len());
4946            for new_obj in new_objects {
4947                let mut obj = new_obj;
4948                if let ObjectKind::Segment {
4949                    segment: crate::front::Segment::Point(point),
4950                } = &mut obj.kind
4951                {
4952                    let new_freedom = point.freedom;
4953                    // When freedom_analysis=false, new values are defaults (Free).
4954                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4955                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4956                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4957                    // when there are actually unsatisfied constraints.
4958                    match new_freedom {
4959                        Freedom::Free => {
4960                            match self.point_freedom_cache.get(&obj.id).copied() {
4961                                Some(Freedom::Conflict) => {
4962                                    // Don't preserve Conflict - conflicts are transient
4963                                    // Keep it as Free
4964                                }
4965                                Some(Freedom::Fixed) => {
4966                                    // Preserve Fixed cached value
4967                                    point.freedom = Freedom::Fixed;
4968                                }
4969                                Some(Freedom::Free) => {
4970                                    // If stored is also Free, keep Free (no change needed)
4971                                }
4972                                None => {
4973                                    // If no cached value, keep Free (default)
4974                                }
4975                            }
4976                        }
4977                        Freedom::Fixed => {
4978                            // Use new value (already set)
4979                        }
4980                        Freedom::Conflict => {
4981                            // Use new value (already set)
4982                        }
4983                    }
4984                    // Store the new freedom value (even if it's Free, so we know it was set)
4985                    self.point_freedom_cache.insert(obj.id, point.freedom);
4986                }
4987                updated_objects.push(obj);
4988            }
4989
4990            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4991            self.scene_graph.objects = updated_objects;
4992        }
4993        outcome
4994    }
4995
4996    fn mutate_ast(
4997        &mut self,
4998        ast: &mut ast::Node<ast::Program>,
4999        object_id: ObjectId,
5000        command: AstMutateCommand,
5001    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5002        let sketch_object = self
5003            .scene_graph
5004            .objects
5005            .get(object_id.0)
5006            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5007        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5008    }
5009
5010    fn mutate_constraint_label_position(
5011        &mut self,
5012        ast: &mut ast::Node<ast::Program>,
5013        constraint_id: ObjectId,
5014        label_position: Point2d<Number>,
5015    ) -> Result<(), KclError> {
5016        let object = self
5017            .scene_graph
5018            .objects
5019            .get(constraint_id.0)
5020            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5021        if !matches!(
5022            &object.kind,
5023            ObjectKind::Constraint {
5024                constraint: Constraint::Distance(_)
5025                    | Constraint::HorizontalDistance(_)
5026                    | Constraint::VerticalDistance(_)
5027                    | Constraint::Radius(_)
5028                    | Constraint::Diameter(_)
5029                    | Constraint::Angle(_),
5030            }
5031        ) {
5032            return Err(KclError::refactor(format!(
5033                "Object does not support labelPosition: {constraint_id:?}"
5034            )));
5035        }
5036
5037        let label_position = to_ast_point2d_number(&label_position)
5038            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5039        self.mutate_ast(
5040            ast,
5041            constraint_id,
5042            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5043        )?;
5044        Ok(())
5045    }
5046}
5047
5048fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5049    // Look up existing sketch.
5050    let sketch_object = scene_graph
5051        .objects
5052        .get(sketch_id.0)
5053        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5054    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5055        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5056    };
5057    expect_single_node_ref(sketch_object)
5058}
5059
5060fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5061    match &object.source {
5062        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5063            range: *range,
5064            node_path: node_path.clone(),
5065        }),
5066        SourceRef::BackTrace { ranges } => {
5067            let [range] = ranges.as_slice() else {
5068                return Err(KclError::refactor(format!(
5069                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5070                    ranges.len()
5071                )));
5072            };
5073            Ok(AstNodeRef {
5074                range: range.0,
5075                node_path: range.1.clone(),
5076            })
5077        }
5078    }
5079}
5080
5081/// This is a deprecated fall-back implementation. Prefer
5082/// [`only_sketch_block()`] to avoid reliance on source ranges.
5083fn only_sketch_block_from_range(
5084    ast: &mut ast::Node<ast::Program>,
5085    sketch_block_range: SourceRange,
5086    edit_kind: ChangeKind,
5087) -> Result<(), KclError> {
5088    let r1 = sketch_block_range;
5089    let matches_range = |r2: SourceRange| -> bool {
5090        // We may have added items to the sketch block, so the end may not be an
5091        // exact match.
5092        match edit_kind {
5093            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5094            // For edit, we don't know whether it grew or shrank.
5095            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5096            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5097            // No edit should be an exact match.
5098            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5099        }
5100    };
5101    let mut found = false;
5102    for item in ast.body.iter_mut() {
5103        match item {
5104            ast::BodyItem::ImportStatement(_) => {}
5105            ast::BodyItem::ExpressionStatement(node) => {
5106                if matches_range(SourceRange::from(&*node))
5107                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5108                {
5109                    sketch_block.is_being_edited = true;
5110                    found = true;
5111                    break;
5112                }
5113            }
5114            ast::BodyItem::VariableDeclaration(node) => {
5115                if matches_range(SourceRange::from(&node.declaration.init))
5116                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5117                {
5118                    sketch_block.is_being_edited = true;
5119                    found = true;
5120                    break;
5121                }
5122            }
5123            ast::BodyItem::TypeDeclaration(_) => {}
5124            ast::BodyItem::ReturnStatement(node) => {
5125                if matches_range(SourceRange::from(&node.argument))
5126                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5127                {
5128                    sketch_block.is_being_edited = true;
5129                    found = true;
5130                    break;
5131                }
5132            }
5133        }
5134    }
5135    if !found {
5136        return Err(KclError::refactor(format!(
5137            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5138        )));
5139    }
5140
5141    Ok(())
5142}
5143
5144fn only_sketch_block(
5145    ast: &mut ast::Node<ast::Program>,
5146    sketch_block_ref: &AstNodeRef,
5147    edit_kind: ChangeKind,
5148) -> Result<(), KclError> {
5149    let Some(target_node_path) = &sketch_block_ref.node_path else {
5150        #[cfg(target_arch = "wasm32")]
5151        web_sys::console::warn_1(
5152            &format!(
5153                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5154                sketch_block_ref
5155            )
5156            .into(),
5157        );
5158        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5159    };
5160    struct MarkSketchBlockBeingEdited<'a> {
5161        target_node_path: &'a ast::NodePath,
5162    }
5163
5164    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5165        type Break = ();
5166        type Continue = ();
5167
5168        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5169            if let NodeMut::SketchBlock(sketch_block) = node
5170                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5171            {
5172                sketch_block.is_being_edited = true;
5173                return TraversalReturn::new_break(());
5174            }
5175            TraversalReturn::new_continue(())
5176        }
5177
5178        fn finish(&mut self, _node: NodeMut<'_>) {}
5179    }
5180
5181    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5182    let found = dfs_mut(ast, &mut marker).is_break();
5183    if !found {
5184        return Err(KclError::refactor(format!(
5185            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5186        )));
5187    }
5188
5189    Ok(())
5190}
5191
5192fn sketch_on_ast_expr(
5193    ast: &mut ast::Node<ast::Program>,
5194    scene_graph: &SceneGraph,
5195    solid_references: &HashMap<Uuid, SolidAstReference>,
5196    on: &Plane,
5197) -> Result<ast::Expr, KclError> {
5198    match on {
5199        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5200        Plane::Object(object_id) => {
5201            let on_object = scene_graph
5202                .objects
5203                .get(object_id.0)
5204                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5205            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5206                return Ok(face_expr);
5207            }
5208            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5209        }
5210        Plane::PrimitiveFace(face) => {
5211            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5212                KclError::refactor(format!(
5213                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5214                    face.solid_id
5215                ))
5216            })?;
5217            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5218            Ok(create_face_of_ast(solid_expr, face_id_expr))
5219        }
5220    }
5221}
5222
5223fn solid_references_from_variables(
5224    ast: &ast::Node<ast::Program>,
5225    variables: &IndexMap<String, KclValueView>,
5226) -> HashMap<Uuid, SolidAstReference> {
5227    let mut references = HashMap::new();
5228
5229    // In-place operations such as shell reuse their input solid's engine ID.
5230    // Later variables overwrite earlier references so mutations target the result.
5231    for item in &ast.body {
5232        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5233            continue;
5234        };
5235        let name = &declaration.declaration.id.name;
5236        let Some(value) = variables.get(name) else {
5237            continue;
5238        };
5239
5240        match value {
5241            KclValueView::Solid { value } => {
5242                references.insert(
5243                    value.id,
5244                    SolidAstReference {
5245                        variable_name: name.clone(),
5246                        output_index: None,
5247                    },
5248                );
5249            }
5250            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5251                for (output_index, entry) in value.iter().enumerate() {
5252                    if let KclValueView::Solid { value } = entry {
5253                        references.insert(
5254                            value.id,
5255                            SolidAstReference {
5256                                variable_name: name.clone(),
5257                                output_index: Some(output_index),
5258                            },
5259                        );
5260                    }
5261                }
5262            }
5263            _ => {}
5264        }
5265    }
5266
5267    references
5268}
5269
5270fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5271    let reference = solid_references.get(&solid_id)?;
5272    let solid_expr = ast_name_expr(reference.variable_name.clone());
5273    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5274}
5275
5276fn sketch_face_of_scene_object_ast_expr(
5277    ast: &mut ast::Node<ast::Program>,
5278    on_object: &crate::front::Object,
5279) -> Result<Option<ast::Expr>, KclError> {
5280    match &on_object.kind {
5281        ObjectKind::Wall(wall) => {
5282            let solid_ref = get_or_insert_ast_reference(
5283                ast,
5284                &source_ref_from_source_ref_range(&wall.source.solid),
5285                "solid",
5286                None,
5287            )?;
5288            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5289                return Err(KclError::refactor(format!(
5290                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5291                    on_object.artifact_id
5292                )));
5293            };
5294            let solid_expr = indexed_solid_expr_for_sweep_output(
5295                ast_name_expr(solid_name_expr.name.name.clone()),
5296                wall.solid_output_index,
5297            );
5298            let sweep_ref = get_or_insert_ast_reference(
5299                ast,
5300                &source_ref_from_source_ref_range(&wall.source.sweep),
5301                "solid",
5302                None,
5303            )?;
5304            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5305                return Err(KclError::refactor(format!(
5306                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5307                    on_object.artifact_id
5308                )));
5309            };
5310            let sweep_name = sweep_name_expr.name.name.clone();
5311            let segment_ref = get_or_insert_ast_reference(
5312                ast,
5313                &source_ref_from_source_ref_range(&wall.source.segment),
5314                LINE_VARIABLE,
5315                None,
5316            )?;
5317
5318            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5319                wall.source
5320                    .path
5321                    .as_ref()
5322                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5323            }) {
5324                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5325                    return Err(KclError::refactor(format!(
5326                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5327                        on_object.artifact_id
5328                    )));
5329                };
5330                create_member_expression(
5331                    create_member_expression(ast_name_expr(region_name), "tags"),
5332                    &segment_name_expr.name.name,
5333                )
5334            } else {
5335                segment_ref
5336            };
5337
5338            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5339        }
5340        ObjectKind::Cap(cap) => {
5341            let solid_ref =
5342                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5343            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5344                return Err(KclError::refactor(format!(
5345                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5346                    on_object.artifact_id
5347                )));
5348            };
5349            let solid_expr = indexed_solid_expr_for_sweep_output(
5350                ast_name_expr(solid_name_expr.name.name.clone()),
5351                cap.solid_output_index,
5352            );
5353            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5354            let face_expr = match cap.kind {
5355                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5356                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5357            };
5358
5359            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5360        }
5361        _ => Ok(None),
5362    }
5363}
5364
5365fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5366    match solid_output_index {
5367        Some(output_index) => create_index_expression(solid_expr, output_index),
5368        None => solid_expr,
5369    }
5370}
5371
5372fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5373    SourceRef::Simple {
5374        range: source.range,
5375        node_path: source.node_path.clone(),
5376    }
5377}
5378
5379fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5380    let source_ref = source_ref_from_source_ref_range(path_source);
5381    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5382    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5383        return None;
5384    };
5385    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5386        return None;
5387    };
5388    if region_call.callee.name.name != "region" {
5389        return None;
5390    }
5391    Some(candidate)
5392}
5393
5394fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5395    let mut current_id = source_id;
5396    let mut current_composite = None;
5397    let mut visited = HashSet::new();
5398
5399    while visited.insert(current_id) {
5400        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5401
5402        let Some(composite_id) = next_composite_id else {
5403            break;
5404        };
5405        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5406            break;
5407        };
5408
5409        current_id = composite.id;
5410        current_composite = Some(composite);
5411
5412        if !composite.consumed {
5413            break;
5414        }
5415    }
5416
5417    current_composite.map(|composite| &composite.code_ref)
5418}
5419
5420fn downstream_composite_id_for_solid_source(
5421    artifact_graph: &ArtifactGraph,
5422    source_id: ArtifactId,
5423) -> Option<ArtifactId> {
5424    // Source is a path, find its solid.
5425    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5426        && let Some(composite_id) = path.composite_solid_id
5427        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5428        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5429    {
5430        return Some(composite_id);
5431    }
5432
5433    // Source is a sweep, find its path -> then find the solid
5434    for artifact in artifact_graph.values() {
5435        if let Artifact::Path(path) = artifact
5436            && path.sweep_id == Some(source_id)
5437            && let Some(composite_id) = path.composite_solid_id
5438            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5439            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5440        {
5441            return Some(composite_id);
5442        }
5443    }
5444
5445    // Source is a solid, find its downstream solid.
5446    artifact_graph.values().find_map(|artifact| {
5447        let Artifact::CompositeSolid(composite) = artifact else {
5448            return None;
5449        };
5450        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5451    })
5452}
5453
5454fn composite_contains_path_input(
5455    solid_ids: &[ArtifactId],
5456    tool_ids: &[ArtifactId],
5457    path_id: ArtifactId,
5458    solid2d_id: Option<ArtifactId>,
5459) -> bool {
5460    composite_contains_input(solid_ids, tool_ids, path_id)
5461        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5462}
5463
5464fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5465    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5466}
5467
5468fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5469    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5470    SourceRefRange {
5471        range: code_ref.range,
5472        node_path,
5473    }
5474}
5475
5476fn solid_output_index_for_sweep(
5477    artifact_graph: &ArtifactGraph,
5478    sweep_id: ArtifactId,
5479    sweep_code_ref: &CodeRef,
5480) -> Option<usize> {
5481    // Constituent sweeps are implementation details of one composite body,
5482    // even when cloning gives them the same CodeRef as the composite root.
5483    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5484        return None;
5485    }
5486
5487    let sibling_sweeps = artifact_graph
5488        .values()
5489        .filter_map(|artifact| match artifact {
5490            Artifact::Sweep(sweep)
5491                if sweep.code_ref.range == sweep_code_ref.range
5492                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5493            {
5494                Some(sweep)
5495            }
5496            _ => None,
5497        })
5498        .collect::<Vec<_>>();
5499
5500    if sibling_sweeps.len() <= 1 {
5501        return None;
5502    }
5503
5504    sibling_sweeps
5505        .iter()
5506        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5507}
5508
5509fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5510    let mut existing_artifact_ids = scene_objects
5511        .iter()
5512        .map(|object| object.artifact_id)
5513        .collect::<HashSet<_>>();
5514
5515    for artifact in artifact_graph.values() {
5516        match artifact {
5517            Artifact::Wall(wall) => {
5518                if existing_artifact_ids.contains(&wall.id) {
5519                    continue;
5520                }
5521
5522                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5523                    Artifact::Segment(segment) => Some(segment),
5524                    _ => None,
5525                }) else {
5526                    continue;
5527                };
5528                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5529                    Artifact::Sweep(sweep) => Some(sweep),
5530                    _ => None,
5531                }) else {
5532                    continue;
5533                };
5534                let source_segment = segment
5535                    .original_seg_id
5536                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5537                    .and_then(|artifact| match artifact {
5538                        Artifact::Segment(segment) => Some(segment),
5539                        _ => None,
5540                    })
5541                    .unwrap_or(segment);
5542                let solid_code_ref =
5543                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5544                let path_code_ref = artifact_graph
5545                    .get(&segment.path_id)
5546                    .or_else(|| artifact_graph.get(&sweep.path_id))
5547                    .and_then(|artifact| match artifact {
5548                        Artifact::Path(path) => Some(&path.code_ref),
5549                        _ => None,
5550                    });
5551                let source = WallSource {
5552                    solid: code_ref_source_ref_range(solid_code_ref),
5553                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5554                    path: path_code_ref.map(code_ref_source_ref_range),
5555                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5556                };
5557                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5558                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5559                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5560                    .flatten();
5561                let object_source = source_ref_from_source_ref_range(&source.solid);
5562                let id = ObjectId(scene_objects.len());
5563                scene_objects.push(crate::front::Object {
5564                    id,
5565                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5566                        id,
5567                        source,
5568                        solid_output_index,
5569                    }),
5570                    label: Default::default(),
5571                    comments: Default::default(),
5572                    artifact_id: wall.id,
5573                    source: object_source,
5574                });
5575                existing_artifact_ids.insert(wall.id);
5576            }
5577            Artifact::Cap(cap) => {
5578                if existing_artifact_ids.contains(&cap.id) {
5579                    continue;
5580                }
5581
5582                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5583                    Artifact::Sweep(sweep) => Some(sweep),
5584                    _ => None,
5585                }) else {
5586                    continue;
5587                };
5588                let id = ObjectId(scene_objects.len());
5589                let kind = match cap.sub_type {
5590                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5591                    CapSubType::End => crate::frontend::api::CapKind::End,
5592                };
5593                let solid_code_ref =
5594                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5595                let source = CapSource {
5596                    solid: code_ref_source_ref_range(solid_code_ref),
5597                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5598                };
5599                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5600                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5601                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5602                    .flatten();
5603                let object_source = source_ref_from_source_ref_range(&source.solid);
5604                scene_objects.push(crate::front::Object {
5605                    id,
5606                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5607                        id,
5608                        kind,
5609                        source,
5610                        solid_output_index,
5611                    }),
5612                    label: Default::default(),
5613                    comments: Default::default(),
5614                    artifact_id: cap.id,
5615                    source: object_source,
5616                });
5617                existing_artifact_ids.insert(cap.id);
5618            }
5619            _ => {}
5620        }
5621    }
5622}
5623
5624fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5625    use crate::engine::PlaneName;
5626
5627    match name {
5628        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5629        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5630        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5631        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5632        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5633        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5634    }
5635}
5636
5637fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5638    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5639        ast::UnaryOperator::Neg,
5640        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5641    )))
5642}
5643
5644fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5645    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5646        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5647        unlabeled: Some(solid_expr),
5648        arguments: vec![ast::LabeledArg {
5649            label: Some(ast::Identifier::new("face")),
5650            arg: face_expr,
5651        }],
5652        digest: None,
5653        non_code_meta: Default::default(),
5654    })))
5655}
5656
5657fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5658    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5660        unlabeled: Some(solid_expr),
5661        arguments: vec![ast::LabeledArg {
5662            label: Some(ast::Identifier::new("index")),
5663            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5664                ast::NumericLiteral {
5665                    value: index as f64,
5666                    suffix: NumericSuffix::None,
5667                    raw: index.to_string(),
5668                    digest: None,
5669                },
5670            )))),
5671        }],
5672        digest: None,
5673        non_code_meta: Default::default(),
5674    })))
5675}
5676
5677fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5678    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5679        return None;
5680    };
5681    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5682        return None;
5683    };
5684    if !matches!(
5685        sweep_call.callee.name.name.as_str(),
5686        "extrude" | "revolve" | "sweep" | "loft"
5687    ) {
5688        return None;
5689    }
5690    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5691        return None;
5692    };
5693    let candidate = region_name_expr.name.name.clone();
5694    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5695        return None;
5696    };
5697    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5698        return None;
5699    };
5700    if region_call.callee.name.name != "region" {
5701        return None;
5702    }
5703    Some(candidate)
5704}
5705
5706/// Return the AST expression referencing the variable at the given source ref.
5707/// If no such variable exists, insert a new variable declaration with the given
5708/// prefix.
5709///
5710/// This may return a complex expression referencing properties of the variable
5711/// (e.g., `line1.start`).
5712fn get_or_insert_ast_reference(
5713    ast: &mut ast::Node<ast::Program>,
5714    source_ref: &SourceRef,
5715    prefix: &str,
5716    property: Option<&str>,
5717) -> Result<ast::Expr, KclError> {
5718    let command = AstMutateCommand::AddVariableDeclaration {
5719        prefix: prefix.to_owned(),
5720    };
5721    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5722        Ok((_, ret)) => ret,
5723        Err(err) => {
5724            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5725                AstMutateCommandReturn::Name(var_name)
5726            } else {
5727                return Err(err);
5728            }
5729        }
5730    };
5731    let AstMutateCommandReturn::Name(var_name) = ret else {
5732        return Err(KclError::refactor(
5733            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5734        ));
5735    };
5736    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5737    let Some(property) = property else {
5738        // No property; just return the variable name.
5739        return Ok(var_expr);
5740    };
5741
5742    Ok(create_member_expression(var_expr, property))
5743}
5744
5745fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5746    let source_range = match source_ref {
5747        SourceRef::Simple { range, .. } => *range,
5748        SourceRef::BackTrace { ranges } => {
5749            let [range] = ranges.as_slice() else {
5750                return None;
5751            };
5752            range.0
5753        }
5754    };
5755    ast.body.iter().find_map(|item| {
5756        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5757            return None;
5758        };
5759        let init_range = SourceRange::from(&var_decl.declaration.init);
5760        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5761            && init_range.start() <= source_range.start()
5762            && source_range.end() <= init_range.end();
5763        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5764            && init_range != source_range
5765            && source_is_inside_init
5766        {
5767            return None;
5768        }
5769        source_is_inside_init.then(|| var_decl.name().to_owned())
5770    })
5771}
5772
5773fn mutate_ast_node_by_source_ref(
5774    ast: &mut ast::Node<ast::Program>,
5775    source_ref: &SourceRef,
5776    command: AstMutateCommand,
5777) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5778    let (source_range, node_path) = match source_ref {
5779        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5780        SourceRef::BackTrace { ranges } => {
5781            let [range] = ranges.as_slice() else {
5782                return Err(KclError::refactor(format!(
5783                    "Expected single source ref, got {}; ranges={ranges:#?}",
5784                    ranges.len(),
5785                )));
5786            };
5787            (range.0, range.1.clone())
5788        }
5789    };
5790    let mut context = AstMutateContext {
5791        source_range,
5792        node_path,
5793        command,
5794        defined_names_stack: Default::default(),
5795    };
5796    let control = dfs_mut(ast, &mut context);
5797    match control {
5798        ControlFlow::Continue(_) => Err(KclError::refactor(
5799            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5800        )),
5801        ControlFlow::Break(break_value) => break_value,
5802    }
5803}
5804
5805#[derive(Debug)]
5806struct AstMutateContext {
5807    source_range: SourceRange,
5808    node_path: Option<ast::NodePath>,
5809    command: AstMutateCommand,
5810    defined_names_stack: Vec<HashSet<String>>,
5811}
5812
5813#[derive(Debug)]
5814#[allow(clippy::large_enum_variant)]
5815enum AstMutateCommand {
5816    /// Add an expression statement to the sketch block.
5817    AddSketchBlockExprStmt {
5818        expr: ast::Expr,
5819    },
5820    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5821    AddSketchBlockVarDecl {
5822        prefix: String,
5823        expr: ast::Expr,
5824    },
5825    AddVariableDeclaration {
5826        prefix: String,
5827    },
5828    EditPoint {
5829        at: ast::Expr,
5830    },
5831    EditLine {
5832        start: ast::Expr,
5833        end: ast::Expr,
5834        construction: Option<bool>,
5835    },
5836    EditArc {
5837        start: ast::Expr,
5838        end: ast::Expr,
5839        center: ast::Expr,
5840        direction: Option<ArcDirection>,
5841        construction: Option<bool>,
5842    },
5843    EditCircle {
5844        start: ast::Expr,
5845        center: ast::Expr,
5846        construction: Option<bool>,
5847    },
5848    EditControlPointSpline {
5849        points: ast::Expr,
5850        construction: Option<bool>,
5851    },
5852    EditConstraintValue {
5853        value: ast::BinaryPart,
5854    },
5855    EditAngleConstraint {
5856        call: ast::BinaryPart,
5857        value: ast::BinaryPart,
5858    },
5859    EditDistanceConstraint {
5860        call: ast::BinaryPart,
5861        value: ast::BinaryPart,
5862    },
5863    EditDistanceConstraintLabelPosition {
5864        label_position: ast::Expr,
5865    },
5866    EditCallUnlabeled {
5867        arg: ast::Expr,
5868    },
5869    EditVarInitialValue {
5870        value: Number,
5871    },
5872    DeleteNode,
5873}
5874
5875impl AstMutateCommand {
5876    fn needs_defined_names_stack(&self) -> bool {
5877        matches!(
5878            self,
5879            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5880        )
5881    }
5882}
5883
5884#[derive(Debug)]
5885enum AstMutateCommandReturn {
5886    None,
5887    Name(String),
5888}
5889
5890#[derive(Debug, Clone)]
5891struct AstNodeRef {
5892    range: SourceRange,
5893    node_path: Option<ast::NodePath>,
5894}
5895
5896impl<T> From<&ast::Node<T>> for AstNodeRef {
5897    fn from(value: &ast::Node<T>) -> Self {
5898        AstNodeRef {
5899            range: value.into(),
5900            node_path: value.node_path.clone(),
5901        }
5902    }
5903}
5904
5905impl From<&ast::BodyItem> for AstNodeRef {
5906    fn from(value: &ast::BodyItem) -> Self {
5907        match value {
5908            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5909                range: node.into(),
5910                node_path: node.node_path.clone(),
5911            },
5912            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5913                range: node.into(),
5914                node_path: node.node_path.clone(),
5915            },
5916            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5917                range: node.into(),
5918                node_path: node.node_path.clone(),
5919            },
5920            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5921                range: node.into(),
5922                node_path: node.node_path.clone(),
5923            },
5924            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5925                range: node.into(),
5926                node_path: node.node_path.clone(),
5927            },
5928        }
5929    }
5930}
5931
5932impl From<&ast::Expr> for AstNodeRef {
5933    fn from(value: &ast::Expr) -> Self {
5934        AstNodeRef {
5935            range: SourceRange::from(value),
5936            node_path: value.node_path().cloned(),
5937        }
5938    }
5939}
5940
5941impl From<&AstMutateContext> for AstNodeRef {
5942    fn from(value: &AstMutateContext) -> Self {
5943        AstNodeRef {
5944            range: value.source_range,
5945            node_path: value.node_path.clone(),
5946        }
5947    }
5948}
5949
5950impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5951    type Error = crate::walk::AstNodeError;
5952
5953    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5954        Ok(AstNodeRef {
5955            range: SourceRange::try_from(value)?,
5956            node_path: value.try_into()?,
5957        })
5958    }
5959}
5960
5961impl From<AstNodeRef> for SourceRange {
5962    fn from(value: AstNodeRef) -> Self {
5963        value.range
5964    }
5965}
5966
5967impl Visitor for AstMutateContext {
5968    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5969    type Continue = ();
5970
5971    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5972        filter_and_process(self, node)
5973    }
5974
5975    fn finish(&mut self, node: NodeMut<'_>) {
5976        match &node {
5977            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5978                self.defined_names_stack.pop();
5979            }
5980            _ => {}
5981        }
5982    }
5983}
5984
5985fn filter_and_process(
5986    ctx: &mut AstMutateContext,
5987    node: NodeMut,
5988) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5989    let Ok(node_range) = SourceRange::try_from(&node) else {
5990        // Nodes that can't be converted to a range aren't interesting.
5991        return TraversalReturn::new_continue(());
5992    };
5993    // If we're adding a variable declaration, we need to look at variable
5994    // declaration expressions to see if it already has a variable, before
5995    // continuing. The variable declaration's source range won't match the
5996    // target; its init expression will.
5997    if let NodeMut::VariableDeclaration(var_decl) = &node {
5998        let expr_range = SourceRange::from(&var_decl.declaration.init);
5999        let expr_node_path = var_decl.declaration.init.node_path();
6000        if source_ref_matches(ctx, expr_range, expr_node_path) {
6001            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6002                // We found the variable declaration expression. It doesn't need
6003                // to be added.
6004                return TraversalReturn::new_break(Ok((
6005                    AstNodeRef::from(&**var_decl),
6006                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6007                )));
6008            }
6009            if let AstMutateCommand::DeleteNode = &ctx.command {
6010                // We found the variable declaration. Delete the variable along
6011                // with the segment.
6012                return TraversalReturn {
6013                    mutate_body_item: MutateBodyItem::Delete,
6014                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6015                };
6016            }
6017        }
6018    }
6019    // Similar thing with expression statement. We need to look at the
6020    // expression inside it.
6021    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6022        let expr_range = SourceRange::from(&expr_stmt.expression);
6023        let expr_node_path = expr_stmt.expression.node_path();
6024        if source_ref_matches(ctx, expr_range, expr_node_path) {
6025            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6026                // We found the node wrapped in an expression statement. Process
6027                // the statement.
6028                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6029                    return TraversalReturn::new_continue(());
6030                };
6031                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6032            }
6033            if let AstMutateCommand::DeleteNode = &ctx.command {
6034                // We found the node wrapped in an expression statement. Delete
6035                // the whole statement.
6036                return TraversalReturn {
6037                    mutate_body_item: MutateBodyItem::Delete,
6038                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6039                };
6040            }
6041        }
6042    }
6043
6044    if ctx.command.needs_defined_names_stack() {
6045        if let NodeMut::Program(program) = &node {
6046            ctx.defined_names_stack.push(find_defined_names(*program));
6047        } else if let NodeMut::SketchBlock(block) = &node {
6048            ctx.defined_names_stack.push(find_defined_names(&block.body));
6049        }
6050    }
6051
6052    // Make sure the node matches the source ref.
6053    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6054    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6055        return TraversalReturn::new_continue(());
6056    }
6057    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6058        return TraversalReturn::new_continue(());
6059    };
6060    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6061}
6062
6063fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6064    match &ctx.node_path {
6065        Some(target) => Some(target) == node_path,
6066        None => node_range == ctx.source_range,
6067    }
6068}
6069
6070fn is_angle_constraint_call_name(name: &str) -> bool {
6071    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6072}
6073
6074fn is_distance_constraint_call_name(name: &str) -> bool {
6075    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6076}
6077
6078fn is_constraint_call_name(name: &str) -> bool {
6079    matches!(
6080        name,
6081        DISTANCE_FN
6082            | HORIZONTAL_DISTANCE_FN
6083            | VERTICAL_DISTANCE_FN
6084            | RADIUS_FN
6085            | DIAMETER_FN
6086            | ANGLE_FN
6087            | ANGLE_DIMENSION_FN
6088    )
6089}
6090
6091fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6092    if let ast::BinaryPart::CallExpressionKw(call) = part
6093        && is_constraint_call_name(call.callee.name.name.as_str())
6094    {
6095        Some(call)
6096    } else {
6097        None
6098    }
6099}
6100
6101fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6102    match &ctx.command {
6103        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6104            if let NodeMut::SketchBlock(sketch_block) = node {
6105                sketch_block
6106                    .body
6107                    .items
6108                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6109                        inner: ast::ExpressionStatement {
6110                            expression: expr.clone(),
6111                            digest: None,
6112                        },
6113                        start: Default::default(),
6114                        end: Default::default(),
6115                        module_id: Default::default(),
6116                        node_path: None,
6117                        outer_attrs: Default::default(),
6118                        pre_comments: Default::default(),
6119                        comment_start: Default::default(),
6120                    }));
6121                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6122            }
6123        }
6124        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6125            if let NodeMut::SketchBlock(sketch_block) = node {
6126                let empty_defined_names = HashSet::new();
6127                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6128                let Ok(name) = next_free_name(prefix, defined_names) else {
6129                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6130                };
6131                sketch_block
6132                    .body
6133                    .items
6134                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6135                        ast::VariableDeclaration::new(
6136                            ast::VariableDeclarator::new(&name, expr.clone()),
6137                            ast::ItemVisibility::Default,
6138                            ast::VariableKind::Const,
6139                        ),
6140                    ))));
6141                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6142            }
6143        }
6144        AstMutateCommand::AddVariableDeclaration { prefix } => {
6145            if let NodeMut::VariableDeclaration(inner) = node {
6146                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6147            }
6148            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6149                let empty_defined_names = HashSet::new();
6150                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6151                let Ok(name) = next_free_name(prefix, defined_names) else {
6152                    // TODO: Return an error instead?
6153                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6154                };
6155                let mutate_node =
6156                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6157                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6158                        ast::ItemVisibility::Default,
6159                        ast::VariableKind::Const,
6160                    ))));
6161                return TraversalReturn {
6162                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6163                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6164                };
6165            }
6166        }
6167        AstMutateCommand::EditPoint { at } => {
6168            if let NodeMut::CallExpressionKw(call) = node {
6169                if call.callee.name.name != POINT_FN {
6170                    return TraversalReturn::new_continue(());
6171                }
6172                // Update the arguments.
6173                for labeled_arg in &mut call.arguments {
6174                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6175                        labeled_arg.arg = at.clone();
6176                    }
6177                }
6178                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6179            }
6180        }
6181        AstMutateCommand::EditLine {
6182            start,
6183            end,
6184            construction,
6185        } => {
6186            if let NodeMut::CallExpressionKw(call) = node {
6187                if call.callee.name.name != LINE_FN {
6188                    return TraversalReturn::new_continue(());
6189                }
6190                // Update the arguments.
6191                for labeled_arg in &mut call.arguments {
6192                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6193                        labeled_arg.arg = start.clone();
6194                    }
6195                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6196                        labeled_arg.arg = end.clone();
6197                    }
6198                }
6199                // Handle construction kwarg
6200                if let Some(construction_value) = construction {
6201                    let construction_exists = call
6202                        .arguments
6203                        .iter()
6204                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6205                    if *construction_value {
6206                        // Add or update construction=true
6207                        if construction_exists {
6208                            // Update existing construction kwarg
6209                            for labeled_arg in &mut call.arguments {
6210                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6211                                    labeled_arg.arg =
6212                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6213                                            value: ast::LiteralValue::Bool(true),
6214                                            raw: "true".to_string(),
6215                                            digest: None,
6216                                        })));
6217                                }
6218                            }
6219                        } else {
6220                            // Add new construction kwarg
6221                            call.arguments.push(ast::LabeledArg {
6222                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6223                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6224                                    value: ast::LiteralValue::Bool(true),
6225                                    raw: "true".to_string(),
6226                                    digest: None,
6227                                }))),
6228                            });
6229                        }
6230                    } else {
6231                        // Remove construction kwarg if it exists
6232                        call.arguments
6233                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6234                    }
6235                }
6236                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6237            }
6238        }
6239        AstMutateCommand::EditArc {
6240            start,
6241            end,
6242            center,
6243            direction,
6244            construction,
6245        } => {
6246            if let NodeMut::CallExpressionKw(call) = node {
6247                if call.callee.name.name != ARC_FN {
6248                    return TraversalReturn::new_continue(());
6249                }
6250                // Update the arguments.
6251                for labeled_arg in &mut call.arguments {
6252                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6253                        labeled_arg.arg = start.clone();
6254                    }
6255                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6256                        labeled_arg.arg = end.clone();
6257                    }
6258                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6259                        labeled_arg.arg = center.clone();
6260                    }
6261                }
6262                // Handle direction kwarg
6263                if let Some(direction_value) = direction {
6264                    let direction_exists = call
6265                        .arguments
6266                        .iter()
6267                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6268                    if direction_value.is_clockwise() {
6269                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6270                        if direction_exists {
6271                            // Update existing direction kwarg
6272                            for labeled_arg in &mut call.arguments {
6273                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6274                                    labeled_arg.arg = direction_ast.clone();
6275                                }
6276                            }
6277                        } else {
6278                            // Add new direction kwarg
6279                            call.arguments.push(ast::LabeledArg {
6280                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6281                                arg: direction_ast,
6282                            });
6283                        }
6284                    } else {
6285                        // Remove direction kwarg if it exists since
6286                        // counterclockwise is the default
6287                        call.arguments
6288                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6289                    }
6290                }
6291                // Handle construction kwarg
6292                if let Some(construction_value) = construction {
6293                    let construction_exists = call
6294                        .arguments
6295                        .iter()
6296                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6297                    if *construction_value {
6298                        // Add or update construction=true
6299                        if construction_exists {
6300                            // Update existing construction kwarg
6301                            for labeled_arg in &mut call.arguments {
6302                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6303                                    labeled_arg.arg =
6304                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6305                                            value: ast::LiteralValue::Bool(true),
6306                                            raw: "true".to_string(),
6307                                            digest: None,
6308                                        })));
6309                                }
6310                            }
6311                        } else {
6312                            // Add new construction kwarg
6313                            call.arguments.push(ast::LabeledArg {
6314                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6315                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6316                                    value: ast::LiteralValue::Bool(true),
6317                                    raw: "true".to_string(),
6318                                    digest: None,
6319                                }))),
6320                            });
6321                        }
6322                    } else {
6323                        // Remove construction kwarg if it exists
6324                        call.arguments
6325                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6326                    }
6327                }
6328                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6329            }
6330        }
6331        AstMutateCommand::EditCircle {
6332            start,
6333            center,
6334            construction,
6335        } => {
6336            if let NodeMut::CallExpressionKw(call) = node {
6337                if call.callee.name.name != CIRCLE_FN {
6338                    return TraversalReturn::new_continue(());
6339                }
6340                // Update the arguments.
6341                for labeled_arg in &mut call.arguments {
6342                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6343                        labeled_arg.arg = start.clone();
6344                    }
6345                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6346                        labeled_arg.arg = center.clone();
6347                    }
6348                }
6349                // Handle construction kwarg
6350                if let Some(construction_value) = construction {
6351                    let construction_exists = call
6352                        .arguments
6353                        .iter()
6354                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6355                    if *construction_value {
6356                        if construction_exists {
6357                            // Update existing construction kwarg
6358                            for labeled_arg in &mut call.arguments {
6359                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6360                                    labeled_arg.arg =
6361                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6362                                            value: ast::LiteralValue::Bool(true),
6363                                            raw: "true".to_string(),
6364                                            digest: None,
6365                                        })));
6366                                }
6367                            }
6368                        } else {
6369                            // Add new construction kwarg
6370                            call.arguments.push(ast::LabeledArg {
6371                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6372                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6373                                    value: ast::LiteralValue::Bool(true),
6374                                    raw: "true".to_string(),
6375                                    digest: None,
6376                                }))),
6377                            });
6378                        }
6379                    } else {
6380                        // Remove construction kwarg if it exists
6381                        call.arguments
6382                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6383                    }
6384                }
6385                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6386            }
6387        }
6388        AstMutateCommand::EditControlPointSpline { points, construction } => {
6389            if let NodeMut::CallExpressionKw(call) = node {
6390                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6391                    return TraversalReturn::new_continue(());
6392                }
6393                for labeled_arg in &mut call.arguments {
6394                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6395                    {
6396                        labeled_arg.arg = points.clone();
6397                    }
6398                }
6399                // Handle construction kwarg
6400                if let Some(construction_value) = construction {
6401                    let construction_exists = call
6402                        .arguments
6403                        .iter()
6404                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6405                    if *construction_value {
6406                        if construction_exists {
6407                            for labeled_arg in &mut call.arguments {
6408                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6409                                    labeled_arg.arg =
6410                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6411                                            value: ast::LiteralValue::Bool(true),
6412                                            raw: "true".to_string(),
6413                                            digest: None,
6414                                        })));
6415                                }
6416                            }
6417                        } else {
6418                            call.arguments.push(ast::LabeledArg {
6419                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6420                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421                                    value: ast::LiteralValue::Bool(true),
6422                                    raw: "true".to_string(),
6423                                    digest: None,
6424                                }))),
6425                            });
6426                        }
6427                    } else {
6428                        call.arguments
6429                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6430                    }
6431                }
6432                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6433            }
6434        }
6435        AstMutateCommand::EditConstraintValue { value } => {
6436            if let NodeMut::BinaryExpression(binary_expr) = node {
6437                let left_is_constraint = matches!(
6438                    &binary_expr.left,
6439                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6440                );
6441                if left_is_constraint {
6442                    binary_expr.right = value.clone();
6443                } else {
6444                    binary_expr.left = value.clone();
6445                }
6446
6447                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6448            }
6449        }
6450        AstMutateCommand::EditAngleConstraint { call, value } => {
6451            if let NodeMut::BinaryExpression(binary_expr) = node {
6452                let left_is_angle = matches!(
6453                    &binary_expr.left,
6454                    ast::BinaryPart::CallExpressionKw(existing_call)
6455                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6456                );
6457                let right_is_angle = matches!(
6458                    &binary_expr.right,
6459                    ast::BinaryPart::CallExpressionKw(existing_call)
6460                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6461                );
6462
6463                match (left_is_angle, right_is_angle) {
6464                    (true, _) => {
6465                        binary_expr.left = call.clone();
6466                        binary_expr.right = value.clone();
6467                    }
6468                    (false, true) => {
6469                        binary_expr.left = value.clone();
6470                        binary_expr.right = call.clone();
6471                    }
6472                    (false, false) => return TraversalReturn::new_continue(()),
6473                }
6474
6475                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6476            }
6477        }
6478        AstMutateCommand::EditDistanceConstraint { call, value } => {
6479            if let NodeMut::BinaryExpression(binary_expr) = node {
6480                let left_is_distance = matches!(
6481                    &binary_expr.left,
6482                    ast::BinaryPart::CallExpressionKw(existing_call)
6483                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6484                );
6485                let right_is_distance = matches!(
6486                    &binary_expr.right,
6487                    ast::BinaryPart::CallExpressionKw(existing_call)
6488                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6489                );
6490
6491                match (left_is_distance, right_is_distance) {
6492                    (true, _) => {
6493                        binary_expr.left = call.clone();
6494                        binary_expr.right = value.clone();
6495                    }
6496                    (false, true) => {
6497                        binary_expr.left = value.clone();
6498                        binary_expr.right = call.clone();
6499                    }
6500                    (false, false) => return TraversalReturn::new_continue(()),
6501                }
6502
6503                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6504            }
6505        }
6506        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6507            if let NodeMut::BinaryExpression(binary_expr) = node {
6508                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6509                    call
6510                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6511                    call
6512                } else {
6513                    return TraversalReturn::new_continue(());
6514                };
6515
6516                if let Some(label_arg) = call
6517                    .arguments
6518                    .iter_mut()
6519                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6520                {
6521                    label_arg.arg = label_position.clone();
6522                } else {
6523                    call.arguments.push(ast::LabeledArg {
6524                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6525                        arg: label_position.clone(),
6526                    });
6527                }
6528
6529                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6530            }
6531        }
6532        AstMutateCommand::EditCallUnlabeled { arg } => {
6533            if let NodeMut::CallExpressionKw(call) = node {
6534                call.unlabeled = Some(arg.clone());
6535                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536            }
6537        }
6538        AstMutateCommand::EditVarInitialValue { value } => {
6539            // We target the SketchVar itself (matched by NodePath) rather than
6540            // the inner NumericLiteral so we can also write back into vars that
6541            // were declared without an initial value (e.g. bare `var`).
6542            if let NodeMut::SketchVar(sketch_var) = node {
6543                let Ok(literal) = to_source_number(*value) else {
6544                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6545                        "Could not convert number to AST literal: {:?}",
6546                        *value
6547                    ))));
6548                };
6549                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6550                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6551            }
6552        }
6553        AstMutateCommand::DeleteNode => {
6554            return TraversalReturn {
6555                mutate_body_item: MutateBodyItem::Delete,
6556                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6557            };
6558        }
6559    }
6560    TraversalReturn::new_continue(())
6561}
6562
6563struct FindSketchBlockSourceRange {
6564    /// The source range of the sketch block before mutation.
6565    target_before_mutation: SourceRange,
6566    /// The source range of the sketch block's last body item after mutation. We
6567    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6568    /// shared reference.
6569    found: Cell<Option<AstNodeRef>>,
6570}
6571
6572impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6573    type Error = crate::front::Error;
6574
6575    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6576        let Ok(node_range) = SourceRange::try_from(&node) else {
6577            return Ok(true);
6578        };
6579
6580        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6581            if node_range.module_id() == self.target_before_mutation.module_id()
6582                && node_range.start() == self.target_before_mutation.start()
6583                // End shouldn't match since we added something.
6584                && node_range.end() >= self.target_before_mutation.end()
6585            {
6586                self.found.set(sketch_block.body.items.last().map(|item| match item {
6587                    // For declarations like `circle1 = circle(...)`, use
6588                    // the init expression range so lookup in source_range_to_object
6589                    // matches the segment source range.
6590                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6591                    _ => AstNodeRef::from(item),
6592                }));
6593                return Ok(false);
6594            } else {
6595                // We found a different sketch block. No need to descend into
6596                // its children since sketch blocks cannot be nested.
6597                return Ok(true);
6598            }
6599        }
6600
6601        for child in node.children().iter() {
6602            if !child.visit(*self)? {
6603                return Ok(false);
6604            }
6605        }
6606
6607        Ok(true)
6608    }
6609}
6610
6611struct FindSketchBlockByNodePath {
6612    /// The Node Path of the sketch block before mutation.
6613    target_node_path: ast::NodePath,
6614    /// The ref of the sketch block's last body item after mutation. We need to
6615    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6616    /// reference.
6617    found: Cell<Option<AstNodeRef>>,
6618}
6619
6620impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6621    type Error = crate::front::Error;
6622
6623    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6624        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6625            return Ok(true);
6626        };
6627
6628        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6629            if let Some(node_path) = node_path
6630                && node_path == self.target_node_path
6631            {
6632                self.found.set(sketch_block.body.items.last().map(|item| match item {
6633                    // For declarations like `circle1 = circle(...)`, use
6634                    // the init expression range so lookup in source_range_to_object
6635                    // matches the segment source range.
6636                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6637                    _ => AstNodeRef::from(item),
6638                }));
6639
6640                return Ok(false);
6641            } else {
6642                // We found a different sketch block. No need to descend into
6643                // its children since sketch blocks cannot be nested.
6644                return Ok(true);
6645            }
6646        }
6647
6648        for child in node.children().iter() {
6649            if !child.visit(*self)? {
6650                return Ok(false);
6651            }
6652        }
6653
6654        Ok(true)
6655    }
6656}
6657
6658/// After adding an item to a sketch block, find the sketch block, and get the
6659/// source range of the added item. We assume that the added item is the last
6660/// item in the sketch block and that the sketch block's source range has grown,
6661/// but not moved from its starting offset.
6662///
6663/// TODO: Do we need to format *before* mutation in case formatting moves the
6664/// sketch block forward?
6665fn find_sketch_block_added_item(
6666    ast: &ast::Node<ast::Program>,
6667    sketch_block_before_mutation: &AstNodeRef,
6668) -> Result<AstNodeRef, KclError> {
6669    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6670        let find = FindSketchBlockByNodePath {
6671            target_node_path: node_path.clone(),
6672            found: Cell::new(None),
6673        };
6674        let node = crate::walk::Node::from(ast);
6675        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6676        find.found.into_inner().ok_or_else(|| {
6677            KclError::refactor(format!(
6678                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6679            ))
6680        })
6681    } else {
6682        // No NodePath. Fall back to legacy source range.
6683        let find = FindSketchBlockSourceRange {
6684            target_before_mutation: sketch_block_before_mutation.range,
6685            found: Cell::new(None),
6686        };
6687        let node = crate::walk::Node::from(ast);
6688        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689        find.found.into_inner().ok_or_else(|| KclError::refactor(
6690            format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
6691        ))
6692    }
6693}
6694
6695fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6696    let message = error.message().trim();
6697    let message = if message.is_empty() {
6698        "unknown parse error"
6699    } else {
6700        message
6701    };
6702
6703    format!("{prefix}: {message}")
6704}
6705
6706fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6707    let (program, errors) = Program::parse(source).map_err(|err| {
6708        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6709    })?;
6710    if !errors.is_empty() {
6711        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6712            format_compilation_issues(parse_error_prefix, &errors),
6713        )));
6714    }
6715
6716    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6717}
6718
6719fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6720    let Some(first_issue) = issues
6721        .iter()
6722        .find(|issue| issue.severity.is_err())
6723        .or_else(|| issues.first())
6724    else {
6725        return prefix.to_owned();
6726    };
6727
6728    let message = first_issue.message.trim();
6729    let message = if message.is_empty() {
6730        "unknown parse error"
6731    } else {
6732        message
6733    };
6734
6735    if issues.len() > 1 {
6736        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6737    } else {
6738        format!("{prefix}: {message}")
6739    }
6740}
6741
6742fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6743    // TODO: Don't duplicate this from lib.rs Program.
6744    ast.recast_top(&Default::default(), 0)
6745}
6746
6747struct FindNumericLiteral {
6748    target: SourceRange,
6749    found: Cell<Option<ast::NumericLiteral>>,
6750}
6751
6752impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6753    type Error = crate::front::Error;
6754
6755    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6756        let Ok(node_range) = SourceRange::try_from(&node) else {
6757            return Ok(true);
6758        };
6759
6760        if node_range == self.target
6761            && let crate::walk::Node::NumericLiteral(literal) = node
6762        {
6763            self.found.set(Some(literal.inner.clone()));
6764            return Ok(false);
6765        }
6766
6767        for child in node.children().iter() {
6768            if !child.visit(*self)? {
6769                return Ok(false);
6770            }
6771        }
6772
6773        Ok(true)
6774    }
6775}
6776
6777fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6778    let find = FindNumericLiteral {
6779        target,
6780        found: Cell::new(None),
6781    };
6782    let node = crate::walk::Node::from(ast);
6783    node.visit(&find).ok()?;
6784    find.found.into_inner()
6785}
6786
6787struct FindSketchVarInitialByNodePath<'a> {
6788    target: &'a ast::NodePath,
6789    sketch_var_found: Cell<bool>,
6790    initial_literal: Cell<Option<ast::NumericLiteral>>,
6791}
6792
6793impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6794    type Error = crate::front::Error;
6795
6796    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6797        if let crate::walk::Node::SketchVar(sketch_var) = node
6798            && sketch_var.node_path.as_ref() == Some(self.target)
6799        {
6800            self.sketch_var_found.set(true);
6801            if let Some(initial) = &sketch_var.initial {
6802                self.initial_literal.set(Some(initial.inner.clone()));
6803            }
6804            return Ok(false);
6805        }
6806
6807        for child in node.children().iter() {
6808            if !child.visit(*self)? {
6809                return Ok(false);
6810            }
6811        }
6812
6813        Ok(true)
6814    }
6815}
6816
6817/// Locate the source `var` declaration corresponding to a sketch-var solution.
6818///
6819/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6820/// from "target found." The inner [`Option`] is the initial numeric literal of
6821/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6822///
6823/// When `node_path` is `None` (e.g. for older outcomes that predate the
6824/// node-path propagation), this falls back to source-range matching, which
6825/// can break under whitespace shifts elsewhere in the file.
6826fn numeric_literal_at_node_path(
6827    ast: &ast::Node<ast::Program>,
6828    node_path: Option<&ast::NodePath>,
6829    source_range: SourceRange,
6830) -> Option<Option<ast::NumericLiteral>> {
6831    let Some(node_path) = node_path else {
6832        let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6833        #[cfg(target_arch = "wasm32")]
6834        web_sys::console::warn_1(&message.into());
6835        #[cfg(not(target_arch = "wasm32"))]
6836        eprintln!("WARNING: {message}");
6837        return numeric_literal_at_source_range(ast, source_range).map(Some);
6838    };
6839    let find = FindSketchVarInitialByNodePath {
6840        target: node_path,
6841        sketch_var_found: Cell::new(false),
6842        initial_literal: Cell::new(None),
6843    };
6844    let node = crate::walk::Node::from(ast);
6845    node.visit(&find).ok()?;
6846    if !find.sketch_var_found.get() {
6847        return None;
6848    }
6849    Some(find.initial_literal.into_inner())
6850}
6851
6852fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6853    match suffix {
6854        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6855        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6856        NumericSuffix::M => Some(UnitLength::Meters),
6857        NumericSuffix::Inch => Some(UnitLength::Inches),
6858        NumericSuffix::Ft => Some(UnitLength::Feet),
6859        NumericSuffix::Yd => Some(UnitLength::Yards),
6860        _ => None,
6861    }
6862}
6863
6864fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6865    match suffix_length_unit(number.units) {
6866        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6867        None => number.value,
6868    }
6869}
6870
6871fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6872    match suffix_length_unit(literal.suffix) {
6873        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6874        None => literal.value,
6875    }
6876}
6877
6878fn var_solution_needs_commit(
6879    current_literal: &ast::NumericLiteral,
6880    solved_value: Number,
6881    default_length_unit: UnitLength,
6882) -> bool {
6883    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6884    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6885
6886    (current - solved).abs() > 1e-9
6887}
6888
6889fn preserve_var_solution_literal_style(
6890    current_literal: &ast::NumericLiteral,
6891    solved_value: Number,
6892    default_length_unit: UnitLength,
6893) -> Number {
6894    if current_literal.suffix == NumericSuffix::None {
6895        return Number {
6896            value: number_value_in_default_length_units(solved_value, default_length_unit),
6897            units: NumericSuffix::None,
6898        };
6899    }
6900
6901    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6902        return solved_value;
6903    };
6904
6905    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6906    Number {
6907        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6908        units: current_literal.suffix,
6909    }
6910}
6911
6912pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6913    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6914        inner: ast::ArrayExpression {
6915            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6916            non_code_meta: Default::default(),
6917            digest: None,
6918        },
6919        start: Default::default(),
6920        end: Default::default(),
6921        module_id: Default::default(),
6922        node_path: None,
6923        outer_attrs: Default::default(),
6924        pre_comments: Default::default(),
6925        comment_start: Default::default(),
6926    })))
6927}
6928
6929pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6930    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6931        ast::ArrayExpression {
6932            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6933            digest: None,
6934            non_code_meta: Default::default(),
6935        },
6936    ))))
6937}
6938
6939fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6940    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941        ast::ArrayExpression {
6942            elements: vec![
6943                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6944                    point.x,
6945                )?)))),
6946                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6947                    point.y,
6948                )?)))),
6949            ],
6950            non_code_meta: Default::default(),
6951            digest: None,
6952        },
6953    ))))
6954}
6955
6956fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6957    match expr {
6958        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6959            inner: ast::Literal::from(to_source_number(*number)?),
6960            start: Default::default(),
6961            end: Default::default(),
6962            module_id: Default::default(),
6963            node_path: None,
6964            outer_attrs: Default::default(),
6965            pre_comments: Default::default(),
6966            comment_start: Default::default(),
6967        }))),
6968        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6969            inner: ast::SketchVar {
6970                initial: Some(BoxNode::new(ast::Node {
6971                    inner: to_source_number(*number)?,
6972                    start: Default::default(),
6973                    end: Default::default(),
6974                    module_id: Default::default(),
6975                    node_path: None,
6976                    outer_attrs: Default::default(),
6977                    pre_comments: Default::default(),
6978                    comment_start: Default::default(),
6979                })),
6980                digest: None,
6981            },
6982            start: Default::default(),
6983            end: Default::default(),
6984            module_id: Default::default(),
6985            node_path: None,
6986            outer_attrs: Default::default(),
6987            pre_comments: Default::default(),
6988            comment_start: Default::default(),
6989        }))),
6990        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6991    }
6992}
6993
6994fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6995    Ok(ast::NumericLiteral {
6996        value: number.value,
6997        suffix: number.units,
6998        raw: format_number_literal(number.value, number.units, None)?,
6999        digest: None,
7000    })
7001}
7002
7003pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7004    ast::Expr::Name(BoxNode::new(ast_name(name)))
7005}
7006
7007fn ast_name(name: String) -> ast::Node<ast::Name> {
7008    ast::Node {
7009        inner: ast::Name {
7010            name: ast::Node {
7011                inner: ast::Identifier { name, digest: None },
7012                start: Default::default(),
7013                end: Default::default(),
7014                module_id: Default::default(),
7015                node_path: None,
7016                outer_attrs: Default::default(),
7017                pre_comments: Default::default(),
7018                comment_start: Default::default(),
7019            },
7020            path: Vec::new(),
7021            abs_path: false,
7022            digest: None,
7023        },
7024        start: Default::default(),
7025        end: Default::default(),
7026        module_id: Default::default(),
7027        node_path: None,
7028        outer_attrs: Default::default(),
7029        pre_comments: Default::default(),
7030        comment_start: Default::default(),
7031    }
7032}
7033
7034pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7035    ast::Name {
7036        name: ast::Node {
7037            inner: ast::Identifier {
7038                name: name.to_owned(),
7039                digest: None,
7040            },
7041            start: Default::default(),
7042            end: Default::default(),
7043            module_id: Default::default(),
7044            node_path: None,
7045            outer_attrs: Default::default(),
7046            pre_comments: Default::default(),
7047            comment_start: Default::default(),
7048        },
7049        path: Default::default(),
7050        abs_path: false,
7051        digest: None,
7052    }
7053}
7054
7055/// Create an AST node for coincident([expr1, expr2, ...])
7056pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7057    let elements = exprs.into_iter().collect::<Vec<_>>();
7058    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7059
7060    // Create array [expr1, expr2, ...]
7061    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7062        elements,
7063        digest: None,
7064        non_code_meta: Default::default(),
7065    })));
7066
7067    // Create coincident([...])
7068    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7069        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7070        unlabeled: Some(array_expr),
7071        arguments: Default::default(),
7072        digest: None,
7073        non_code_meta: Default::default(),
7074    })))
7075}
7076
7077/// Create an AST node for horizontal(line)
7078pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7079    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7080        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7081        unlabeled: Some(line_expr),
7082        arguments: Default::default(),
7083        digest: None,
7084        non_code_meta: Default::default(),
7085    })))
7086}
7087
7088/// Create an AST node for vertical(line)
7089pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7090    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7092        unlabeled: Some(line_expr),
7093        arguments: Default::default(),
7094        digest: None,
7095        non_code_meta: Default::default(),
7096    })))
7097}
7098
7099/// Create a member expression like object.property (e.g., line1.end)
7100pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7101    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7102        object: object_expr,
7103        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7104            name: ast::Node::no_src(ast::Identifier {
7105                name: property.to_string(),
7106                digest: None,
7107            }),
7108            path: Vec::new(),
7109            abs_path: false,
7110            digest: None,
7111        }))),
7112        computed: false,
7113        digest: None,
7114    })))
7115}
7116
7117pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7118    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7119        object: object_expr,
7120        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7121            ast::NumericLiteral {
7122                value: index as f64,
7123                suffix: NumericSuffix::None,
7124                raw: index.to_string(),
7125                digest: None,
7126            },
7127        )))),
7128        computed: true,
7129        digest: None,
7130    })))
7131}
7132
7133/// Create an AST node for `fixed([point, [x, y]])`.
7134fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7135    // Create [x, y] array literal.
7136    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7137        position.x,
7138    )?))));
7139    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7140        position.y,
7141    )?))));
7142    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7143        elements: vec![x_literal, y_literal],
7144        digest: None,
7145        non_code_meta: Default::default(),
7146    })));
7147
7148    // Create [point, [x, y]] outer array.
7149    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7150        elements: vec![point_expr, point_array],
7151        digest: None,
7152        non_code_meta: Default::default(),
7153    })));
7154
7155    // Create fixed([...])
7156    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7157        ast::CallExpressionKw {
7158            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7159            unlabeled: Some(array_expr),
7160            arguments: Default::default(),
7161            digest: None,
7162            non_code_meta: Default::default(),
7163        },
7164    ))))
7165}
7166
7167/// Create an AST node for equalLength([line1, line2, ...])
7168pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7169    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7170        elements: line_exprs,
7171        digest: None,
7172        non_code_meta: Default::default(),
7173    })));
7174
7175    // Create equalLength([...])
7176    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7177        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7178        unlabeled: Some(array_expr),
7179        arguments: Default::default(),
7180        digest: None,
7181        non_code_meta: Default::default(),
7182    })))
7183}
7184
7185/// Create an AST node for equalRadius([seg1, seg2, ...])
7186pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7187    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7188        elements: segment_exprs,
7189        digest: None,
7190        non_code_meta: Default::default(),
7191    })));
7192
7193    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7194        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7195        unlabeled: Some(array_expr),
7196        arguments: Default::default(),
7197        digest: None,
7198        non_code_meta: Default::default(),
7199    })))
7200}
7201
7202/// Create an AST node for tangent([seg1, seg2])
7203pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7204    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205        elements: vec![seg1_expr, seg2_expr],
7206        digest: None,
7207        non_code_meta: Default::default(),
7208    })));
7209
7210    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7211        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7212        unlabeled: Some(array_expr),
7213        arguments: Default::default(),
7214        digest: None,
7215        non_code_meta: Default::default(),
7216    })))
7217}
7218
7219/// Create an AST node for symmetric([input1, input2], axis = line)
7220pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7221    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7222        elements: input_exprs,
7223        digest: None,
7224        non_code_meta: Default::default(),
7225    })));
7226    let arguments = vec![ast::LabeledArg {
7227        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7228        arg: axis_expr,
7229    }];
7230
7231    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7232        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7233        unlabeled: Some(array_expr),
7234        arguments,
7235        digest: None,
7236        non_code_meta: Default::default(),
7237    })))
7238}
7239
7240/// Create an AST node for midpoint(segment, point = point)
7241pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7242    let arguments = vec![ast::LabeledArg {
7243        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7244        arg: point_expr,
7245    }];
7246
7247    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7248        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7249        unlabeled: Some(segment_expr),
7250        arguments,
7251        digest: None,
7252        non_code_meta: Default::default(),
7253    })))
7254}
7255
7256/// The source range to attach to a diagnostic for `error` in the top-level
7257/// module. Source ranges are ordered innermost first and may point into
7258/// imported modules, so prefer the innermost range that is in the top-level
7259/// module; otherwise fall back to the innermost range.
7260fn issue_source_range(error: &KclError) -> SourceRange {
7261    let source_ranges = error.source_ranges();
7262    source_ranges
7263        .iter()
7264        .find(|range| range.is_top_level_module())
7265        .or_else(|| source_ranges.first())
7266        .copied()
7267        .unwrap_or_else(SourceRange::synthetic)
7268}
7269
7270#[cfg(test)]
7271mod tests {
7272    use std::sync;
7273
7274    use super::*;
7275    use crate::engine::PlaneName;
7276    use crate::engine::engine_manager::EngineManager;
7277    use crate::execution::cache::SketchModeState;
7278    use crate::execution::cache::clear_mem_cache;
7279    use crate::execution::cache::read_old_memory;
7280    use crate::execution::cache::write_old_memory;
7281    use crate::front::Distance;
7282    use crate::front::Fixed;
7283    use crate::front::FixedPoint;
7284    use crate::front::Midpoint;
7285    use crate::front::Object;
7286    use crate::front::Plane;
7287    use crate::front::Sketch;
7288    use crate::front::Tangent;
7289    use crate::frontend::sketch::Vertical;
7290    use crate::pretty::NumericSuffix;
7291
7292    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7293        for object in &scene_graph.objects {
7294            if let ObjectKind::Sketch(_) = &object.kind {
7295                return Some(object);
7296            }
7297        }
7298        None
7299    }
7300
7301    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7302        for object in &scene_graph.objects {
7303            if let ObjectKind::Face(_) = &object.kind {
7304                return Some(object);
7305            }
7306        }
7307        None
7308    }
7309
7310    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7311        for object in &scene_graph.objects {
7312            if matches!(&object.kind, ObjectKind::Wall(_)) {
7313                return Some(object.id);
7314            }
7315        }
7316        None
7317    }
7318
7319    fn find_cap_object_id_with_solid_output_index(
7320        scene_graph: &SceneGraph,
7321        cap_kind: crate::frontend::api::CapKind,
7322        solid_output_index: usize,
7323    ) -> Option<ObjectId> {
7324        for object in &scene_graph.objects {
7325            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7326            {
7327                return Some(object.id);
7328            }
7329        }
7330        None
7331    }
7332
7333    #[test]
7334    fn issue_source_range_prefers_top_level_module() {
7335        use kcl_error::ModuleId;
7336
7337        let top = SourceRange::new(10, 20, ModuleId::default());
7338        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7339
7340        // Innermost frame is in an imported module; the diagnostic should
7341        // land on the innermost top-level range instead.
7342        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7343            "boom".to_owned(),
7344            vec![imported, top],
7345        ));
7346        assert_eq!(super::issue_source_range(&error), top);
7347
7348        // No top-level range at all: fall back to the innermost one.
7349        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7350        assert_eq!(super::issue_source_range(&error), imported);
7351
7352        // No ranges: synthetic.
7353        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7354        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7355    }
7356
7357    #[test]
7358    fn composite_constituent_sweeps_are_not_solid_outputs() {
7359        use kcl_api::artifact::ArtifactSweepMethod;
7360        use kcl_api::artifact::CompositeSolid;
7361        use kcl_api::artifact::CompositeSolidSubType;
7362        use kcl_api::artifact::Sweep;
7363        use kcl_api::artifact::SweepSubType;
7364
7365        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7366        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7367        let composite_id = ArtifactId::new(Uuid::new_v4());
7368        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7369        let sweep = |id| {
7370            Artifact::Sweep(Sweep {
7371                id,
7372                sub_type: SweepSubType::Extrusion,
7373                path_id: ArtifactId::new(Uuid::new_v4()),
7374                surface_ids: Vec::new(),
7375                edge_ids: Vec::new(),
7376                code_ref: code_ref.clone(),
7377                source_sweep_id: None,
7378                trajectory_id: None,
7379                method: ArtifactSweepMethod::New,
7380                consumed: false,
7381                pattern_ids: Vec::new(),
7382            })
7383        };
7384        let mut artifacts = IndexMap::from([
7385            (first_sweep_id, sweep(first_sweep_id)),
7386            (second_sweep_id, sweep(second_sweep_id)),
7387        ]);
7388
7389        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7390        assert_eq!(
7391            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7392            Some(0)
7393        );
7394        assert_eq!(
7395            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7396            Some(1)
7397        );
7398
7399        artifacts.insert(
7400            composite_id,
7401            Artifact::CompositeSolid(CompositeSolid {
7402                id: composite_id,
7403                consumed: false,
7404                sub_type: CompositeSolidSubType::Union,
7405                output_index: None,
7406                solid_ids: vec![first_sweep_id, second_sweep_id],
7407                tool_ids: Vec::new(),
7408                code_ref,
7409                composite_solid_id: None,
7410                pattern_ids: Vec::new(),
7411            }),
7412        );
7413        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7414        assert_eq!(
7415            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7416            None
7417        );
7418        assert_eq!(
7419            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7420            None
7421        );
7422    }
7423
7424    #[test]
7425    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7426        let source = "\
7427region001 = region(point = [0.1, 0.1], sketch = s)
7428extrude001 = extrude(region001, length = 5)
7429revolve001 = revolve(region001, axis = Y)
7430sweep001 = sweep(region001, path = path001)
7431loft001 = loft(region001)
7432not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7433";
7434
7435        let program = Program::parse(source).unwrap().0.unwrap();
7436
7437        assert_eq!(
7438            region_name_from_sweep_variable(&program.ast, "extrude001"),
7439            Some("region001".to_owned())
7440        );
7441        assert_eq!(
7442            region_name_from_sweep_variable(&program.ast, "revolve001"),
7443            Some("region001".to_owned())
7444        );
7445        assert_eq!(
7446            region_name_from_sweep_variable(&program.ast, "sweep001"),
7447            Some("region001".to_owned())
7448        );
7449        assert_eq!(
7450            region_name_from_sweep_variable(&program.ast, "loft001"),
7451            Some("region001".to_owned())
7452        );
7453        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7454    }
7455
7456    #[track_caller]
7457    fn expect_sketch(object: &Object) -> &Sketch {
7458        if let ObjectKind::Sketch(sketch) = &object.kind {
7459            sketch
7460        } else {
7461            panic!("Object is not a sketch: {:?}", object);
7462        }
7463    }
7464
7465    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7466        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7467        let ObjectKind::Segment {
7468            segment: Segment::Point(point),
7469        } = &point_object.kind
7470        else {
7471            panic!("Object is not a point segment: {point_object:?}");
7472        };
7473        point.position.clone()
7474    }
7475
7476    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7477        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7478        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7479    }
7480
7481    /// Build a millimeter-valued point expression for concise sketch edit test
7482    /// setup.
7483    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7484        Point2d {
7485            x: Expr::Var(Number {
7486                value: x,
7487                units: NumericSuffix::Mm,
7488            }),
7489            y: Expr::Var(Number {
7490                value: y,
7491                units: NumericSuffix::Mm,
7492            }),
7493        }
7494    }
7495
7496    /// Build a millimeter-valued numeric point for comparing solved scene graph
7497    /// positions.
7498    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7499        Point2d {
7500            x: Number {
7501                value: x,
7502                units: NumericSuffix::Mm,
7503            },
7504            y: Number {
7505                value: y,
7506                units: NumericSuffix::Mm,
7507            },
7508        }
7509    }
7510
7511    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7512        LineCtor {
7513            start: Point2d {
7514                x: Expr::Number(Number { value: start_x, units }),
7515                y: Expr::Number(Number { value: start_y, units }),
7516            },
7517            end: Point2d {
7518                x: Expr::Number(Number { value: end_x, units }),
7519                y: Expr::Number(Number { value: end_y, units }),
7520            },
7521            construction: None,
7522        }
7523    }
7524
7525    async fn create_sketch_with_single_line(
7526        frontend: &mut FrontendState,
7527        ctx: &ExecutorContext,
7528        mock_ctx: &ExecutorContext,
7529        version: Version,
7530    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7531        frontend.program = Program::empty();
7532
7533        let sketch_args = SketchCtor {
7534            on: Plane::Default(PlaneName::Xy),
7535        };
7536        let (_src_delta, _scene_delta, sketch_id) = frontend
7537            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7538            .await
7539            .unwrap();
7540
7541        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7542        let (source_delta, scene_graph_delta) = frontend
7543            .add_segment(mock_ctx, version, sketch_id, segment, None)
7544            .await
7545            .unwrap();
7546        let line_id = *scene_graph_delta
7547            .new_objects
7548            .last()
7549            .expect("Expected line object id to be created");
7550
7551        (sketch_id, line_id, source_delta, scene_graph_delta)
7552    }
7553
7554    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7555        frontend.program = program.clone();
7556        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7557        frontend.update_state_after_exec(outcome, true);
7558    }
7559
7560    #[test]
7561    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7562        for (source, expected_message) in [
7563            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7564            (
7565                "3'",
7566                "Error parsing KCL source after editing: unterminated string literal",
7567            ),
7568        ] {
7569            let err = parse_frontend_mutation_source(
7570                source,
7571                "Error parsing KCL source after editing",
7572                "No AST produced after editing",
7573            )
7574            .expect_err("expected invalid KCL source to fail");
7575            let message = err.error.message();
7576
7577            assert_eq!(message, expected_message);
7578            assert!(!message.contains("CompilationIssue"));
7579            assert!(!message.contains("KclErrorDetails"));
7580            assert!(!message.contains("source_range"));
7581        }
7582    }
7583
7584    #[tokio::test(flavor = "multi_thread")]
7585    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7586        let initial_source = "\
7587sketch(on = XY) {
7588  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7589  distance([line1.start, line1.end]) == 10
7590}
7591";
7592        let program = Program::parse(initial_source).unwrap().0.unwrap();
7593
7594        let mut frontend = FrontendState::new();
7595        let mock_ctx = ExecutorContext::new_mock(None).await;
7596        let version = Version(0);
7597
7598        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7599        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7600        let sketch_id = sketch_object.id;
7601        let sketch = expect_sketch(sketch_object);
7602        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7603
7604        for (value, expected_message) in [
7605            ("**", "Invalid constraint value: Unexpected token: *"),
7606            ("3'", "Invalid constraint value: unterminated string literal"),
7607        ] {
7608            let err = frontend
7609                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7610                .await
7611                .expect_err("expected invalid constraint expression to fail");
7612            let message = err.error.message();
7613
7614            assert_eq!(message, expected_message);
7615            assert!(!message.contains("CompilationIssue"));
7616            assert!(!message.contains("KclErrorDetails"));
7617            assert!(!message.contains("source_range"));
7618        }
7619
7620        mock_ctx.close().await;
7621    }
7622
7623    #[tokio::test(flavor = "multi_thread")]
7624    async fn test_failed_edit_constraint_value_does_not_update_program() {
7625        let initial_source = "\
7626sketch(on = XY) {
7627  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7628  distance([line1.start, line1.end]) == 10
7629}
7630";
7631        let program = Program::parse(initial_source).unwrap().0.unwrap();
7632        let original_source = program.original_file_contents.clone();
7633
7634        let mut frontend = FrontendState::new();
7635        let mock_ctx = ExecutorContext::new_mock(None).await;
7636        let version = Version(0);
7637
7638        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7639        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7640        let sketch_id = sketch_object.id;
7641        let sketch = expect_sketch(sketch_object);
7642        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7643
7644        frontend
7645            .edit_constraint_value(
7646                &mock_ctx,
7647                version,
7648                sketch_id,
7649                constraint_id,
7650                "unknownDistance".to_owned(),
7651            )
7652            .await
7653            .expect_err("expected invalid constraint value to fail execution");
7654
7655        assert_eq!(frontend.program.original_file_contents, original_source);
7656        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7657
7658        mock_ctx.close().await;
7659    }
7660
7661    #[tokio::test(flavor = "multi_thread")]
7662    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7663        let initial_source = "\
7664arr = [0]
7665sketch(on = XY) {
7666  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7667  distance([line1.start, line1.end]) == 10
7668}
7669";
7670        let program = Program::parse(initial_source).unwrap().0.unwrap();
7671
7672        let mut frontend = FrontendState::new();
7673        let mock_ctx = ExecutorContext::new_mock(None).await;
7674        let version = Version(0);
7675
7676        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7677        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7678        let sketch_id = sketch_object.id;
7679        let sketch = expect_sketch(sketch_object);
7680        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7681
7682        // In sketch mode execution, an out-of-bounds array index is an error.
7683        // It should not fall back to the first element of the array the way
7684        // plain mock execution does.
7685        let err = frontend
7686            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7687            .await
7688            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7689        let message = err.error.message();
7690        assert!(
7691            message.contains("The array doesn't have any item at index 5"),
7692            "unexpected error message: {message}"
7693        );
7694
7695        mock_ctx.close().await;
7696    }
7697
7698    #[tokio::test(flavor = "multi_thread")]
7699    async fn test_sketch_checkpoint_round_trip_restores_state() {
7700        let mut frontend = FrontendState::new();
7701        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7702        let mock_ctx = ExecutorContext::new_mock(None).await;
7703        let version = Version(0);
7704
7705        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7706            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7707
7708        let expected_source = source_delta.text.clone();
7709        let expected_scene_graph = frontend.scene_graph.clone();
7710        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7711        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7712
7713        let checkpoint_id = frontend
7714            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7715            .await
7716            .unwrap();
7717
7718        let edited_segments = vec![ExistingSegmentCtor {
7719            id: line_id,
7720            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7721        }];
7722        let (edited_source, _edited_scene) = frontend
7723            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7724            .await
7725            .unwrap();
7726        assert_ne!(edited_source.text, expected_source);
7727
7728        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7729
7730        assert_eq!(restored.source_delta.text, expected_source);
7731        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7732        assert!(restored.scene_graph_delta.invalidates_ids);
7733        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7734        assert_eq!(frontend.scene_graph, expected_scene_graph);
7735        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7736
7737        ctx.close().await;
7738    }
7739
7740    #[tokio::test(flavor = "multi_thread")]
7741    async fn test_sketch_checkpoints_prune_oldest_entries() {
7742        let mut frontend = FrontendState::new();
7743        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7744        let mock_ctx = ExecutorContext::new_mock(None).await;
7745        let version = Version(0);
7746
7747        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7748            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7749
7750        let mut checkpoint_ids = Vec::new();
7751        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7752            checkpoint_ids.push(
7753                frontend
7754                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7755                    .await
7756                    .unwrap(),
7757            );
7758        }
7759
7760        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7761        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7762
7763        let oldest_retained = checkpoint_ids[3];
7764        assert_eq!(
7765            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7766            Some(oldest_retained)
7767        );
7768
7769        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7770        assert!(evicted_restore.is_err());
7771        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7772
7773        frontend
7774            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7775            .await
7776            .unwrap();
7777
7778        ctx.close().await;
7779    }
7780
7781    #[tokio::test(flavor = "multi_thread")]
7782    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7783        let mut frontend = FrontendState::new();
7784        let missing_checkpoint = SketchCheckpointId::new(999);
7785
7786        let err = frontend
7787            .restore_sketch_checkpoint(missing_checkpoint)
7788            .await
7789            .expect_err("Expected restore to fail for missing checkpoint");
7790
7791        assert!(err.msg.contains("Sketch checkpoint not found"));
7792    }
7793
7794    #[tokio::test(flavor = "multi_thread")]
7795    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7796        let mut frontend = FrontendState::new();
7797        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7798        let mock_ctx = ExecutorContext::new_mock(None).await;
7799        let version = Version(0);
7800
7801        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7802            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7803
7804        let checkpoint_a = frontend
7805            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7806            .await
7807            .unwrap();
7808        let checkpoint_b = frontend
7809            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7810            .await
7811            .unwrap();
7812        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7813
7814        frontend.clear_sketch_checkpoints();
7815        assert!(frontend.sketch_checkpoints.is_empty());
7816        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7817        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7818
7819        ctx.close().await;
7820    }
7821
7822    #[tokio::test(flavor = "multi_thread")]
7823    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7824        let mut frontend = FrontendState::new();
7825        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7826        let mock_ctx = ExecutorContext::new_mock(None).await;
7827        let version = Version(0);
7828
7829        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7830            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7831        let old_source = source_delta.text.clone();
7832        let old_checkpoint = frontend
7833            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7834            .await
7835            .unwrap();
7836        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7837
7838        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7839            .unwrap()
7840            .0
7841            .unwrap();
7842
7843        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7844        let SetProgramOutcome::Success {
7845            checkpoint_id: Some(new_checkpoint),
7846            ..
7847        } = result
7848        else {
7849            panic!("Expected Success with a fresh checkpoint baseline");
7850        };
7851
7852        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7853
7854        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7855        assert_eq!(old_restore.source_delta.text, old_source);
7856
7857        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7858        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7859
7860        ctx.close().await;
7861    }
7862
7863    #[tokio::test(flavor = "multi_thread")]
7864    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7865        let mut frontend = FrontendState::new();
7866        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7867        let mock_ctx = ExecutorContext::new_mock(None).await;
7868        let version = Version(0);
7869
7870        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7871            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7872        let old_checkpoint = frontend
7873            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7874            .await
7875            .unwrap();
7876        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7877
7878        let failing_program = Program::parse(
7879            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7880        )
7881        .unwrap()
7882        .0
7883        .unwrap();
7884
7885        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7886        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7887        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7888        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7889
7890        ctx.close().await;
7891    }
7892
7893    #[tokio::test(flavor = "multi_thread")]
7894    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7895        let mut frontend = FrontendState::new();
7896        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7897
7898        let program = Program::parse(
7899            "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",
7900        )
7901        .unwrap()
7902        .0
7903        .unwrap();
7904        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7905        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7906            panic!("Expected successful baseline program execution");
7907        };
7908
7909        clear_mem_cache().await;
7910        assert!(read_old_memory().await.is_none());
7911
7912        let checkpoint_without_mock_memory = frontend
7913            .create_sketch_checkpoint((*exec_outcome).clone())
7914            .await
7915            .unwrap();
7916
7917        write_old_memory(SketchModeState::new_for_tests()).await;
7918        assert!(read_old_memory().await.is_some());
7919
7920        let checkpoint_with_mock_memory = frontend
7921            .create_sketch_checkpoint((*exec_outcome).clone())
7922            .await
7923            .unwrap();
7924
7925        clear_mem_cache().await;
7926        assert!(read_old_memory().await.is_none());
7927
7928        frontend
7929            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7930            .await
7931            .unwrap();
7932        assert!(read_old_memory().await.is_some());
7933
7934        frontend
7935            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7936            .await
7937            .unwrap();
7938        assert!(read_old_memory().await.is_none());
7939
7940        ctx.close().await;
7941    }
7942
7943    #[tokio::test(flavor = "multi_thread")]
7944    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7945        let source = "\
7946sketch(on = XY) {
7947  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7948}
7949
7950bad = missing_name
7951";
7952        let program = Program::parse(source).unwrap().0.unwrap();
7953
7954        let mut frontend = FrontendState::new();
7955
7956        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7957        let mock_ctx = ExecutorContext::new_mock(None).await;
7958        let version = Version(0);
7959        let project_id = ProjectId(0);
7960        let file_id = FileId(0);
7961
7962        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7963            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7964        };
7965
7966        let sketch_id = frontend
7967            .scene_graph
7968            .objects
7969            .iter()
7970            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7971            .expect("Expected sketch object from errored hack_set_program");
7972
7973        frontend
7974            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7975            .await
7976            .unwrap();
7977
7978        ctx.close().await;
7979        mock_ctx.close().await;
7980    }
7981
7982    #[tokio::test(flavor = "multi_thread")]
7983    async fn test_new_sketch_add_point_edit_point() {
7984        let program = Program::empty();
7985
7986        let mut frontend = FrontendState::new();
7987        frontend.program = program;
7988
7989        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7990        let mock_ctx = ExecutorContext::new_mock(None).await;
7991        let version = Version(0);
7992
7993        let sketch_args = SketchCtor {
7994            on: Plane::Default(PlaneName::Xy),
7995        };
7996        let (_src_delta, scene_delta, sketch_id) = frontend
7997            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7998            .await
7999            .unwrap();
8000        assert_eq!(sketch_id, ObjectId(1));
8001        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8002        let sketch_object = &scene_delta.new_graph.objects[1];
8003        assert_eq!(sketch_object.id, ObjectId(1));
8004        assert_eq!(
8005            sketch_object.kind,
8006            ObjectKind::Sketch(Sketch {
8007                args: SketchCtor {
8008                    on: Plane::Default(PlaneName::Xy)
8009                },
8010                plane: ObjectId(0),
8011                segments: vec![],
8012                constraints: vec![],
8013            })
8014        );
8015        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8016
8017        let point_ctor = PointCtor {
8018            position: Point2d {
8019                x: Expr::Number(Number {
8020                    value: 1.0,
8021                    units: NumericSuffix::Inch,
8022                }),
8023                y: Expr::Number(Number {
8024                    value: 2.0,
8025                    units: NumericSuffix::Inch,
8026                }),
8027            },
8028        };
8029        let segment = SegmentCtor::Point(point_ctor);
8030        let (src_delta, scene_delta) = frontend
8031            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8032            .await
8033            .unwrap();
8034        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8035        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8036        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8037        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8038            assert_eq!(scene_object.id.0, i);
8039        }
8040
8041        let point_id = *scene_delta.new_objects.last().unwrap();
8042
8043        let point_ctor = PointCtor {
8044            position: Point2d {
8045                x: Expr::Number(Number {
8046                    value: 3.0,
8047                    units: NumericSuffix::Inch,
8048                }),
8049                y: Expr::Number(Number {
8050                    value: 4.0,
8051                    units: NumericSuffix::Inch,
8052                }),
8053            },
8054        };
8055        let segments = vec![ExistingSegmentCtor {
8056            id: point_id,
8057            ctor: SegmentCtor::Point(point_ctor),
8058        }];
8059        let (src_delta, scene_delta) = frontend
8060            .edit_segments(&mock_ctx, version, sketch_id, segments)
8061            .await
8062            .unwrap();
8063        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8064        assert_eq!(scene_delta.new_objects, vec![]);
8065        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8066
8067        ctx.close().await;
8068        mock_ctx.close().await;
8069    }
8070
8071    #[tokio::test(flavor = "multi_thread")]
8072    async fn test_new_sketch_add_line_edit_line() {
8073        let program = Program::empty();
8074
8075        let mut frontend = FrontendState::new();
8076        frontend.program = program;
8077
8078        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8079        let mock_ctx = ExecutorContext::new_mock(None).await;
8080        let version = Version(0);
8081
8082        let sketch_args = SketchCtor {
8083            on: Plane::Default(PlaneName::Xy),
8084        };
8085        let (_src_delta, scene_delta, sketch_id) = frontend
8086            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8087            .await
8088            .unwrap();
8089        assert_eq!(sketch_id, ObjectId(1));
8090        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8091        let sketch_object = &scene_delta.new_graph.objects[1];
8092        assert_eq!(sketch_object.id, ObjectId(1));
8093        assert_eq!(
8094            sketch_object.kind,
8095            ObjectKind::Sketch(Sketch {
8096                args: SketchCtor {
8097                    on: Plane::Default(PlaneName::Xy)
8098                },
8099                plane: ObjectId(0),
8100                segments: vec![],
8101                constraints: vec![],
8102            })
8103        );
8104        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8105
8106        let line_ctor = LineCtor {
8107            start: Point2d {
8108                x: Expr::Number(Number {
8109                    value: 0.0,
8110                    units: NumericSuffix::Mm,
8111                }),
8112                y: Expr::Number(Number {
8113                    value: 0.0,
8114                    units: NumericSuffix::Mm,
8115                }),
8116            },
8117            end: Point2d {
8118                x: Expr::Number(Number {
8119                    value: 10.0,
8120                    units: NumericSuffix::Mm,
8121                }),
8122                y: Expr::Number(Number {
8123                    value: 10.0,
8124                    units: NumericSuffix::Mm,
8125                }),
8126            },
8127            construction: None,
8128        };
8129        let segment = SegmentCtor::Line(line_ctor);
8130        let (src_delta, scene_delta) = frontend
8131            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8132            .await
8133            .unwrap();
8134        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8135        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8136        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8137        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8138            assert_eq!(scene_object.id.0, i);
8139        }
8140
8141        // The new objects are the end points and then the line.
8142        let line = *scene_delta.new_objects.last().unwrap();
8143
8144        let line_ctor = LineCtor {
8145            start: Point2d {
8146                x: Expr::Number(Number {
8147                    value: 1.0,
8148                    units: NumericSuffix::Mm,
8149                }),
8150                y: Expr::Number(Number {
8151                    value: 2.0,
8152                    units: NumericSuffix::Mm,
8153                }),
8154            },
8155            end: Point2d {
8156                x: Expr::Number(Number {
8157                    value: 13.0,
8158                    units: NumericSuffix::Mm,
8159                }),
8160                y: Expr::Number(Number {
8161                    value: 14.0,
8162                    units: NumericSuffix::Mm,
8163                }),
8164            },
8165            construction: None,
8166        };
8167        let segments = vec![ExistingSegmentCtor {
8168            id: line,
8169            ctor: SegmentCtor::Line(line_ctor),
8170        }];
8171        let (src_delta, scene_delta) = frontend
8172            .edit_segments(&mock_ctx, version, sketch_id, segments)
8173            .await
8174            .unwrap();
8175        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8176        assert_eq!(scene_delta.new_objects, vec![]);
8177        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8178
8179        ctx.close().await;
8180        mock_ctx.close().await;
8181    }
8182
8183    #[tokio::test(flavor = "multi_thread")]
8184    async fn test_new_sketch_add_arc_edit_arc() {
8185        let program = Program::empty();
8186
8187        let mut frontend = FrontendState::new();
8188        frontend.program = program;
8189
8190        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8191        let mock_ctx = ExecutorContext::new_mock(None).await;
8192        let version = Version(0);
8193
8194        let sketch_args = SketchCtor {
8195            on: Plane::Default(PlaneName::Xy),
8196        };
8197        let (_src_delta, scene_delta, sketch_id) = frontend
8198            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8199            .await
8200            .unwrap();
8201        assert_eq!(sketch_id, ObjectId(1));
8202        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8203        let sketch_object = &scene_delta.new_graph.objects[1];
8204        assert_eq!(sketch_object.id, ObjectId(1));
8205        assert_eq!(
8206            sketch_object.kind,
8207            ObjectKind::Sketch(Sketch {
8208                args: SketchCtor {
8209                    on: Plane::Default(PlaneName::Xy),
8210                },
8211                plane: ObjectId(0),
8212                segments: vec![],
8213                constraints: vec![],
8214            })
8215        );
8216        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8217
8218        let arc_ctor = ArcCtor {
8219            start: Point2d {
8220                x: Expr::Var(Number {
8221                    value: 0.0,
8222                    units: NumericSuffix::Mm,
8223                }),
8224                y: Expr::Var(Number {
8225                    value: 0.0,
8226                    units: NumericSuffix::Mm,
8227                }),
8228            },
8229            end: Point2d {
8230                x: Expr::Var(Number {
8231                    value: 10.0,
8232                    units: NumericSuffix::Mm,
8233                }),
8234                y: Expr::Var(Number {
8235                    value: 10.0,
8236                    units: NumericSuffix::Mm,
8237                }),
8238            },
8239            center: Point2d {
8240                x: Expr::Var(Number {
8241                    value: 10.0,
8242                    units: NumericSuffix::Mm,
8243                }),
8244                y: Expr::Var(Number {
8245                    value: 0.0,
8246                    units: NumericSuffix::Mm,
8247                }),
8248            },
8249            direction: None,
8250            construction: None,
8251        };
8252        let segment = SegmentCtor::Arc(arc_ctor);
8253        let (src_delta, scene_delta) = frontend
8254            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8255            .await
8256            .unwrap();
8257        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8258        assert_eq!(
8259            scene_delta.new_objects,
8260            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8261        );
8262        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8263            assert_eq!(scene_object.id.0, i);
8264        }
8265        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8266
8267        // The new objects are the end points, the center, and then the arc.
8268        let arc = *scene_delta.new_objects.last().unwrap();
8269
8270        let arc_ctor = ArcCtor {
8271            start: Point2d {
8272                x: Expr::Var(Number {
8273                    value: 1.0,
8274                    units: NumericSuffix::Mm,
8275                }),
8276                y: Expr::Var(Number {
8277                    value: 2.0,
8278                    units: NumericSuffix::Mm,
8279                }),
8280            },
8281            end: Point2d {
8282                x: Expr::Var(Number {
8283                    value: 13.0,
8284                    units: NumericSuffix::Mm,
8285                }),
8286                y: Expr::Var(Number {
8287                    value: 14.0,
8288                    units: NumericSuffix::Mm,
8289                }),
8290            },
8291            center: Point2d {
8292                x: Expr::Var(Number {
8293                    value: 13.0,
8294                    units: NumericSuffix::Mm,
8295                }),
8296                y: Expr::Var(Number {
8297                    value: 2.0,
8298                    units: NumericSuffix::Mm,
8299                }),
8300            },
8301            direction: None,
8302            construction: None,
8303        };
8304        let segments = vec![ExistingSegmentCtor {
8305            id: arc,
8306            ctor: SegmentCtor::Arc(arc_ctor),
8307        }];
8308        let (src_delta, scene_delta) = frontend
8309            .edit_segments(&mock_ctx, version, sketch_id, segments)
8310            .await
8311            .unwrap();
8312        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8313        assert_eq!(scene_delta.new_objects, vec![]);
8314        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8315
8316        ctx.close().await;
8317        mock_ctx.close().await;
8318    }
8319
8320    #[tokio::test(flavor = "multi_thread")]
8321    async fn test_new_sketch_add_circle_edit_circle() {
8322        let program = Program::empty();
8323
8324        let mut frontend = FrontendState::new();
8325        frontend.program = program;
8326
8327        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8328        let mock_ctx = ExecutorContext::new_mock(None).await;
8329        let version = Version(0);
8330
8331        let sketch_args = SketchCtor {
8332            on: Plane::Default(PlaneName::Xy),
8333        };
8334        let (_src_delta, _scene_delta, sketch_id) = frontend
8335            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8336            .await
8337            .unwrap();
8338
8339        // Add a circle segment.
8340        let circle_ctor = CircleCtor {
8341            start: Point2d {
8342                x: Expr::Var(Number {
8343                    value: 5.0,
8344                    units: NumericSuffix::Mm,
8345                }),
8346                y: Expr::Var(Number {
8347                    value: 0.0,
8348                    units: NumericSuffix::Mm,
8349                }),
8350            },
8351            center: Point2d {
8352                x: Expr::Var(Number {
8353                    value: 0.0,
8354                    units: NumericSuffix::Mm,
8355                }),
8356                y: Expr::Var(Number {
8357                    value: 0.0,
8358                    units: NumericSuffix::Mm,
8359                }),
8360            },
8361            construction: None,
8362        };
8363        let segment = SegmentCtor::Circle(circle_ctor);
8364        let (src_delta, scene_delta) = frontend
8365            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8366            .await
8367            .unwrap();
8368        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8369        // The new objects are start, center, and then the circle segment.
8370        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8371        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8372
8373        let circle = *scene_delta.new_objects.last().unwrap();
8374
8375        // Edit the circle segment.
8376        let circle_ctor = CircleCtor {
8377            start: Point2d {
8378                x: Expr::Var(Number {
8379                    value: 10.0,
8380                    units: NumericSuffix::Mm,
8381                }),
8382                y: Expr::Var(Number {
8383                    value: 0.0,
8384                    units: NumericSuffix::Mm,
8385                }),
8386            },
8387            center: Point2d {
8388                x: Expr::Var(Number {
8389                    value: 3.0,
8390                    units: NumericSuffix::Mm,
8391                }),
8392                y: Expr::Var(Number {
8393                    value: 4.0,
8394                    units: NumericSuffix::Mm,
8395                }),
8396            },
8397            construction: None,
8398        };
8399        let segments = vec![ExistingSegmentCtor {
8400            id: circle,
8401            ctor: SegmentCtor::Circle(circle_ctor),
8402        }];
8403        let (src_delta, scene_delta) = frontend
8404            .edit_segments(&mock_ctx, version, sketch_id, segments)
8405            .await
8406            .unwrap();
8407        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8408        assert_eq!(scene_delta.new_objects, vec![]);
8409        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8410
8411        ctx.close().await;
8412        mock_ctx.close().await;
8413    }
8414
8415    #[tokio::test(flavor = "multi_thread")]
8416    async fn test_delete_circle() {
8417        let initial_source = "sketch001 = sketch(on = XY) {
8418  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8419}
8420";
8421
8422        let program = Program::parse(initial_source).unwrap().0.unwrap();
8423        let mut frontend = FrontendState::new();
8424
8425        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8426        let mock_ctx = ExecutorContext::new_mock(None).await;
8427        let version = Version(0);
8428
8429        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8430        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8431        let sketch_id = sketch_object.id;
8432        let sketch = expect_sketch(sketch_object);
8433
8434        // The sketch should have 3 segments: start point, center point, and the circle.
8435        assert_eq!(sketch.segments.len(), 3);
8436        let circle_id = sketch.segments[2];
8437
8438        // Delete the circle.
8439        let (src_delta, scene_delta) = frontend
8440            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8441            .await
8442            .unwrap();
8443        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8444        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8445        let new_sketch = expect_sketch(new_sketch_object);
8446        assert_eq!(new_sketch.segments.len(), 0);
8447
8448        ctx.close().await;
8449        mock_ctx.close().await;
8450    }
8451
8452    #[tokio::test(flavor = "multi_thread")]
8453    async fn test_edit_circle_via_point() {
8454        let initial_source = "sketch001 = sketch(on = XY) {
8455  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8456}
8457";
8458
8459        let program = Program::parse(initial_source).unwrap().0.unwrap();
8460        let mut frontend = FrontendState::new();
8461
8462        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8463        let mock_ctx = ExecutorContext::new_mock(None).await;
8464        let version = Version(0);
8465
8466        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8468        let sketch_id = sketch_object.id;
8469        let sketch = expect_sketch(sketch_object);
8470
8471        // Find the circle segment and its start point.
8472        let circle_id = sketch
8473            .segments
8474            .iter()
8475            .copied()
8476            .find(|seg_id| {
8477                matches!(
8478                    &frontend.scene_graph.objects[seg_id.0].kind,
8479                    ObjectKind::Segment {
8480                        segment: Segment::Circle(_)
8481                    }
8482                )
8483            })
8484            .expect("Expected a circle segment in sketch");
8485        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8486        let ObjectKind::Segment {
8487            segment: Segment::Circle(circle),
8488        } = &circle_object.kind
8489        else {
8490            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8491        };
8492        let start_point_id = circle.start;
8493
8494        // Edit the start point via SegmentCtor::Point.
8495        let segments = vec![ExistingSegmentCtor {
8496            id: start_point_id,
8497            ctor: SegmentCtor::Point(PointCtor {
8498                position: Point2d {
8499                    x: Expr::Var(Number {
8500                        value: 7.0,
8501                        units: NumericSuffix::Mm,
8502                    }),
8503                    y: Expr::Var(Number {
8504                        value: 1.0,
8505                        units: NumericSuffix::Mm,
8506                    }),
8507                },
8508            }),
8509        }];
8510        let (src_delta, _scene_delta) = frontend
8511            .edit_segments(&mock_ctx, version, sketch_id, segments)
8512            .await
8513            .unwrap();
8514        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8515
8516        ctx.close().await;
8517        mock_ctx.close().await;
8518    }
8519
8520    #[tokio::test(flavor = "multi_thread")]
8521    async fn test_add_line_when_sketch_block_uses_variable() {
8522        let initial_source = "s = sketch(on = XY) {}
8523";
8524
8525        let program = Program::parse(initial_source).unwrap().0.unwrap();
8526
8527        let mut frontend = FrontendState::new();
8528
8529        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8530        let mock_ctx = ExecutorContext::new_mock(None).await;
8531        let version = Version(0);
8532
8533        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8534        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8535        let sketch_id = sketch_object.id;
8536
8537        let line_ctor = LineCtor {
8538            start: Point2d {
8539                x: Expr::Number(Number {
8540                    value: 0.0,
8541                    units: NumericSuffix::Mm,
8542                }),
8543                y: Expr::Number(Number {
8544                    value: 0.0,
8545                    units: NumericSuffix::Mm,
8546                }),
8547            },
8548            end: Point2d {
8549                x: Expr::Number(Number {
8550                    value: 10.0,
8551                    units: NumericSuffix::Mm,
8552                }),
8553                y: Expr::Number(Number {
8554                    value: 10.0,
8555                    units: NumericSuffix::Mm,
8556                }),
8557            },
8558            construction: None,
8559        };
8560        let segment = SegmentCtor::Line(line_ctor);
8561        let (src_delta, scene_delta) = frontend
8562            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8563            .await
8564            .unwrap();
8565        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8566        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8567        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8568
8569        ctx.close().await;
8570        mock_ctx.close().await;
8571    }
8572
8573    #[tokio::test(flavor = "multi_thread")]
8574    async fn test_new_sketch_add_line_delete_sketch() {
8575        let program = Program::empty();
8576
8577        let mut frontend = FrontendState::new();
8578        frontend.program = program;
8579
8580        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8581        let mock_ctx = ExecutorContext::new_mock(None).await;
8582        let version = Version(0);
8583
8584        let sketch_args = SketchCtor {
8585            on: Plane::Default(PlaneName::Xy),
8586        };
8587        let (_src_delta, scene_delta, sketch_id) = frontend
8588            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8589            .await
8590            .unwrap();
8591        assert_eq!(sketch_id, ObjectId(1));
8592        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8593        let sketch_object = &scene_delta.new_graph.objects[1];
8594        assert_eq!(sketch_object.id, ObjectId(1));
8595        assert_eq!(
8596            sketch_object.kind,
8597            ObjectKind::Sketch(Sketch {
8598                args: SketchCtor {
8599                    on: Plane::Default(PlaneName::Xy)
8600                },
8601                plane: ObjectId(0),
8602                segments: vec![],
8603                constraints: vec![],
8604            })
8605        );
8606        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8607
8608        let line_ctor = LineCtor {
8609            start: Point2d {
8610                x: Expr::Number(Number {
8611                    value: 0.0,
8612                    units: NumericSuffix::Mm,
8613                }),
8614                y: Expr::Number(Number {
8615                    value: 0.0,
8616                    units: NumericSuffix::Mm,
8617                }),
8618            },
8619            end: Point2d {
8620                x: Expr::Number(Number {
8621                    value: 10.0,
8622                    units: NumericSuffix::Mm,
8623                }),
8624                y: Expr::Number(Number {
8625                    value: 10.0,
8626                    units: NumericSuffix::Mm,
8627                }),
8628            },
8629            construction: None,
8630        };
8631        let segment = SegmentCtor::Line(line_ctor);
8632        let (src_delta, scene_delta) = frontend
8633            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8634            .await
8635            .unwrap();
8636        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8637        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8638
8639        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8640        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8641        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8642
8643        ctx.close().await;
8644        mock_ctx.close().await;
8645    }
8646
8647    #[tokio::test(flavor = "multi_thread")]
8648    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8649        let initial_source = "s = sketch(on = XY) {}
8650";
8651
8652        let program = Program::parse(initial_source).unwrap().0.unwrap();
8653
8654        let mut frontend = FrontendState::new();
8655
8656        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8657        let version = Version(0);
8658
8659        frontend.hack_set_program(&ctx, program).await.unwrap();
8660        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8661        let sketch_id = sketch_object.id;
8662
8663        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8664        insta::assert_snapshot!(
8665            "test_delete_sketch_when_sketch_block_uses_variable",
8666            src_delta.text.as_str()
8667        );
8668        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8669
8670        ctx.close().await;
8671    }
8672
8673    #[tokio::test(flavor = "multi_thread")]
8674    async fn test_delete_sketch_after_comment() {
8675        let initial_source = "sketch001 = sketch(on = XZ) {
8676}
8677";
8678
8679        let program = Program::parse(initial_source).unwrap().0.unwrap();
8680        let mut frontend = FrontendState::new();
8681
8682        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8683        let version = Version(0);
8684
8685        frontend.hack_set_program(&ctx, program).await.unwrap();
8686        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8687        let sketch_id = sketch_object.id;
8688        let original_source = sketch_object.source.clone();
8689
8690        let commented_source = "// test 1
8691sketch001 = sketch(on = XZ) {
8692}
8693";
8694        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8695        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8696
8697        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8698        assert_eq!(cached_sketch_object.source, original_source);
8699
8700        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8701        assert!(
8702            !src_delta.text.contains("sketch001"),
8703            "sketch was not deleted: {}",
8704            src_delta.text
8705        );
8706        // The leading line comment must survive deletion.
8707        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8708        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8709
8710        ctx.close().await;
8711    }
8712
8713    #[tokio::test(flavor = "multi_thread")]
8714    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8715        let initial_source = "sketch001 = sketch(on = XZ) {
8716}
8717foo = 1
8718";
8719
8720        let program = Program::parse(initial_source).unwrap().0.unwrap();
8721        let mut frontend = FrontendState::new();
8722
8723        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8724        let version = Version(0);
8725
8726        frontend.hack_set_program(&ctx, program).await.unwrap();
8727        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8728        let sketch_id = sketch_object.id;
8729
8730        let commented_source = "// keep me
8731sketch001 = sketch(on = XZ) {
8732}
8733foo = 1
8734";
8735        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8736        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8737
8738        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8739        // The leading comment should remain, now attached to the following body item.
8740        insta::assert_snapshot!(
8741            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8742            src_delta.text.as_str()
8743        );
8744
8745        ctx.close().await;
8746    }
8747
8748    #[tokio::test(flavor = "multi_thread")]
8749    async fn test_delete_segment_preserves_pre_comment() {
8750        let initial_source = "\
8751sketch(on = XY) {
8752  point(at = [var 1, var 2])
8753  // describe the middle point
8754  point(at = [var 3, var 4])
8755  point(at = [var 5, var 6])
8756}
8757";
8758
8759        let program = Program::parse(initial_source).unwrap().0.unwrap();
8760        let mut frontend = FrontendState::new();
8761
8762        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8763        let mock_ctx = ExecutorContext::new_mock(None).await;
8764        let version = Version(0);
8765
8766        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8767        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8768        let sketch_id = sketch_object.id;
8769        let sketch = expect_sketch(sketch_object);
8770
8771        let middle_point_id = *sketch.segments.get(1).unwrap();
8772
8773        let (src_delta, _scene_delta) = frontend
8774            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8775            .await
8776            .unwrap();
8777        // The line comment on the line above the deleted point must be preserved.
8778        // It is reattached to the next surviving body item.
8779        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8780
8781        ctx.close().await;
8782        mock_ctx.close().await;
8783    }
8784
8785    #[tokio::test(flavor = "multi_thread")]
8786    async fn test_delete_last_segment_preserves_pre_comment() {
8787        let initial_source = "\
8788sketch(on = XY) {
8789  point(at = [var 1, var 2])
8790  // describe the trailing point
8791  point(at = [var 3, var 4])
8792}
8793";
8794
8795        let program = Program::parse(initial_source).unwrap().0.unwrap();
8796        let mut frontend = FrontendState::new();
8797
8798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8799        let mock_ctx = ExecutorContext::new_mock(None).await;
8800        let version = Version(0);
8801
8802        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8803        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8804        let sketch_id = sketch_object.id;
8805        let sketch = expect_sketch(sketch_object);
8806
8807        let last_point_id = *sketch.segments.last().unwrap();
8808
8809        let (src_delta, _scene_delta) = frontend
8810            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8811            .await
8812            .unwrap();
8813        // No following item to attach to; the comment is kept inside the sketch
8814        // block as trailing non-code metadata so the user does not lose it.
8815        insta::assert_snapshot!(
8816            "test_delete_last_segment_preserves_pre_comment",
8817            src_delta.text.as_str()
8818        );
8819
8820        ctx.close().await;
8821        mock_ctx.close().await;
8822    }
8823
8824    #[tokio::test(flavor = "multi_thread")]
8825    async fn test_delete_segment_drops_inline_trailing_comment() {
8826        let initial_source = "\
8827sketch(on = XY) {
8828  point(at = [var 1, var 2])
8829  point(at = [var 3, var 4]) // same-line note that gets dropped
8830  point(at = [var 5, var 6])
8831}
8832";
8833
8834        let program = Program::parse(initial_source).unwrap().0.unwrap();
8835        let mut frontend = FrontendState::new();
8836
8837        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8838        let mock_ctx = ExecutorContext::new_mock(None).await;
8839        let version = Version(0);
8840
8841        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8842        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8843        let sketch_id = sketch_object.id;
8844        let sketch = expect_sketch(sketch_object);
8845
8846        let middle_point_id = *sketch.segments.get(1).unwrap();
8847
8848        let (src_delta, _scene_delta) = frontend
8849            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8850            .await
8851            .unwrap();
8852        // The same-line trailing comment is removed along with the deleted code.
8853        assert!(
8854            !src_delta.text.contains("same-line note"),
8855            "inline comment should have been removed: {}",
8856            src_delta.text
8857        );
8858
8859        ctx.close().await;
8860        mock_ctx.close().await;
8861    }
8862
8863    #[tokio::test(flavor = "multi_thread")]
8864    async fn test_delete_segments_preserves_block_comments_across_positions() {
8865        // One test exercising several `delete_body_item_preserving_pre_comments`
8866        // branches at once with `/* ... */` block comments:
8867        //   - first point: leading block comment must migrate to the next item.
8868        //   - first point: same-line trailing block comment must be dropped.
8869        //   - middle point: leading block comment must stay attached after migration.
8870        //   - last point: leading block comment, with no surviving next item,
8871        //     must be converted into a trailing NonCodeNode.
8872        let initial_source = "\
8873sketch(on = XY) {
8874  /* above first - moves to middle */
8875  point(at = [var 1, var 2]) /* same-line on first - dropped */
8876  /* above middle - stays */
8877  point(at = [var 3, var 4])
8878  /* above last - moves to trailing meta */
8879  point(at = [var 5, var 6])
8880}
8881";
8882
8883        let program = Program::parse(initial_source).unwrap().0.unwrap();
8884        let mut frontend = FrontendState::new();
8885
8886        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8887        let mock_ctx = ExecutorContext::new_mock(None).await;
8888        let version = Version(0);
8889
8890        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8892        let sketch_id = sketch_object.id;
8893        let sketch = expect_sketch(sketch_object);
8894
8895        let first_point_id = *sketch.segments.first().unwrap();
8896        let last_point_id = *sketch.segments.last().unwrap();
8897
8898        let (src_delta, _scene_delta) = frontend
8899            .delete_objects(
8900                &mock_ctx,
8901                version,
8902                sketch_id,
8903                Vec::new(),
8904                vec![first_point_id, last_point_id],
8905            )
8906            .await
8907            .unwrap();
8908        insta::assert_snapshot!(
8909            "test_delete_segments_preserves_block_comments_across_positions",
8910            src_delta.text.as_str()
8911        );
8912
8913        ctx.close().await;
8914        mock_ctx.close().await;
8915    }
8916
8917    #[tokio::test(flavor = "multi_thread")]
8918    async fn test_edit_line_when_editing_its_start_point() {
8919        let initial_source = "\
8920sketch(on = XY) {
8921  line(start = [var 1, var 2], end = [var 3, var 4])
8922}
8923";
8924
8925        let program = Program::parse(initial_source).unwrap().0.unwrap();
8926
8927        let mut frontend = FrontendState::new();
8928
8929        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8930        let mock_ctx = ExecutorContext::new_mock(None).await;
8931        let version = Version(0);
8932
8933        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8934        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8935        let sketch_id = sketch_object.id;
8936        let sketch = expect_sketch(sketch_object);
8937
8938        let point_id = *sketch.segments.first().unwrap();
8939
8940        let point_ctor = PointCtor {
8941            position: Point2d {
8942                x: Expr::Var(Number {
8943                    value: 5.0,
8944                    units: NumericSuffix::Inch,
8945                }),
8946                y: Expr::Var(Number {
8947                    value: 6.0,
8948                    units: NumericSuffix::Inch,
8949                }),
8950            },
8951        };
8952        let segments = vec![ExistingSegmentCtor {
8953            id: point_id,
8954            ctor: SegmentCtor::Point(point_ctor),
8955        }];
8956        let (src_delta, scene_delta) = frontend
8957            .edit_segments(&mock_ctx, version, sketch_id, segments)
8958            .await
8959            .unwrap();
8960        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8961        assert_eq!(scene_delta.new_objects, vec![]);
8962        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8963
8964        ctx.close().await;
8965        mock_ctx.close().await;
8966    }
8967
8968    #[tokio::test(flavor = "multi_thread")]
8969    async fn test_edit_line_when_editing_its_end_point() {
8970        let initial_source = "\
8971sketch(on = XY) {
8972  line(start = [var 1, var 2], end = [var 3, var 4])
8973}
8974";
8975
8976        let program = Program::parse(initial_source).unwrap().0.unwrap();
8977
8978        let mut frontend = FrontendState::new();
8979
8980        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8981        let mock_ctx = ExecutorContext::new_mock(None).await;
8982        let version = Version(0);
8983
8984        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8985        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8986        let sketch_id = sketch_object.id;
8987        let sketch = expect_sketch(sketch_object);
8988        let point_id = *sketch.segments.get(1).unwrap();
8989
8990        let point_ctor = PointCtor {
8991            position: Point2d {
8992                x: Expr::Var(Number {
8993                    value: 5.0,
8994                    units: NumericSuffix::Inch,
8995                }),
8996                y: Expr::Var(Number {
8997                    value: 6.0,
8998                    units: NumericSuffix::Inch,
8999                }),
9000            },
9001        };
9002        let segments = vec![ExistingSegmentCtor {
9003            id: point_id,
9004            ctor: SegmentCtor::Point(point_ctor),
9005        }];
9006        let (src_delta, scene_delta) = frontend
9007            .edit_segments(&mock_ctx, version, sketch_id, segments)
9008            .await
9009            .unwrap();
9010        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9011        assert_eq!(scene_delta.new_objects, vec![]);
9012        assert_eq!(
9013            scene_delta.new_graph.objects.len(),
9014            5,
9015            "{:#?}",
9016            scene_delta.new_graph.objects
9017        );
9018
9019        ctx.close().await;
9020        mock_ctx.close().await;
9021    }
9022
9023    #[tokio::test(flavor = "multi_thread")]
9024    async fn test_edit_line_with_coincident_feedback() {
9025        let initial_source = "\
9026sketch(on = XY) {
9027  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9028  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9029  fixed([line1.start, [0, 0]])
9030  coincident([line1.end, line2.start])
9031  equalLength([line1, line2])
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_with_default_client().await.unwrap();
9040        let mock_ctx = ExecutorContext::new_mock(None).await;
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 line2_end_id = *sketch.segments.get(4).unwrap();
9048
9049        let segments = vec![ExistingSegmentCtor {
9050            id: line2_end_id,
9051            ctor: SegmentCtor::Point(PointCtor {
9052                position: Point2d {
9053                    x: Expr::Var(Number {
9054                        value: 9.0,
9055                        units: NumericSuffix::None,
9056                    }),
9057                    y: Expr::Var(Number {
9058                        value: 10.0,
9059                        units: NumericSuffix::None,
9060                    }),
9061                },
9062            }),
9063        }];
9064        let (src_delta, scene_delta) = frontend
9065            .edit_segments(&mock_ctx, version, sketch_id, segments)
9066            .await
9067            .unwrap();
9068        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9069        assert_eq!(
9070            scene_delta.new_graph.objects.len(),
9071            11,
9072            "{:#?}",
9073            scene_delta.new_graph.objects
9074        );
9075
9076        ctx.close().await;
9077        mock_ctx.close().await;
9078    }
9079
9080    #[tokio::test(flavor = "multi_thread")]
9081    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9082        let initial_source = "\
9083sketch(on = XY) {
9084  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9085  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9086  fixed([line1.start, [0, 0]])
9087  coincident([line1.end, line2.start])
9088  equalLength([line1, line2])
9089}
9090";
9091
9092        let program = Program::parse(initial_source).unwrap().0.unwrap();
9093        let mut frontend = FrontendState::new();
9094        let mock_ctx = ExecutorContext::new_mock(None).await;
9095        let version = Version(0);
9096
9097        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9098        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9099        let sketch_id = sketch_object.id;
9100        let sketch = expect_sketch(sketch_object);
9101        let line2_end_id = *sketch.segments.get(4).unwrap();
9102
9103        let segments = vec![ExistingSegmentCtor {
9104            id: line2_end_id,
9105            ctor: SegmentCtor::Point(PointCtor {
9106                position: Point2d {
9107                    x: Expr::Var(Number {
9108                        value: 9.0,
9109                        units: NumericSuffix::None,
9110                    }),
9111                    y: Expr::Var(Number {
9112                        value: 10.0,
9113                        units: NumericSuffix::None,
9114                    }),
9115                },
9116            }),
9117        }];
9118        let (edited_source, _) = frontend
9119            .edit_segments(&mock_ctx, version, sketch_id, segments)
9120            .await
9121            .unwrap();
9122
9123        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9124        assert_eq!(mock_source.text, edited_source.text);
9125
9126        mock_ctx.close().await;
9127    }
9128
9129    /// Preview segment edits should return solved geometry without persisting
9130    /// solver feedback to KCL.
9131    #[tokio::test(flavor = "multi_thread")]
9132    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9133        let initial_source = "\
9134sketch(on = XY) {
9135  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9136  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9137  fixed([line1.start, [0, 0]])
9138  coincident([line1.end, line2.start])
9139  equalLength([line1, line2])
9140}
9141";
9142
9143        let program = Program::parse(initial_source).unwrap().0.unwrap();
9144        let mut frontend = FrontendState::new();
9145        let mock_ctx = ExecutorContext::new_mock(None).await;
9146        let version = Version(0);
9147
9148        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9149        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9150        let sketch_id = sketch_object.id;
9151        let sketch = expect_sketch(sketch_object);
9152        let line2_end_id = *sketch.segments.get(4).unwrap();
9153
9154        let segments = vec![ExistingSegmentCtor {
9155            id: line2_end_id,
9156            ctor: SegmentCtor::Point(PointCtor {
9157                position: Point2d {
9158                    x: Expr::Var(Number {
9159                        value: 9.0,
9160                        units: NumericSuffix::None,
9161                    }),
9162                    y: Expr::Var(Number {
9163                        value: 10.0,
9164                        units: NumericSuffix::None,
9165                    }),
9166                },
9167            }),
9168        }];
9169        let (preview_source, preview_delta) = frontend
9170            .edit_segments_with_options(
9171                &mock_ctx,
9172                version,
9173                sketch_id,
9174                segments,
9175                EditSegmentsOptions {
9176                    anchor_segment_ids: Some(vec![line2_end_id]),
9177                    drag_anchors: Vec::new(),
9178                    constraint_label_edits: Vec::new(),
9179                    commit_solved_initial_guesses: false,
9180                },
9181            )
9182            .await
9183            .unwrap();
9184
9185        assert!(
9186            !preview_delta.exec_outcome.var_solutions.is_empty(),
9187            "preview solve should still solve and return geometry feedback"
9188        );
9189        assert!(
9190            preview_source
9191                .text
9192                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9193        );
9194        assert!(
9195            preview_source
9196                .text
9197                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9198        );
9199
9200        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9201        assert_eq!(mock_source.text, preview_source.text);
9202
9203        mock_ctx.close().await;
9204    }
9205
9206    #[tokio::test(flavor = "multi_thread")]
9207    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9208        let initial_source = "\
9209sketch(on = XY) {
9210  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9211}
9212";
9213
9214        let program = Program::parse(initial_source).unwrap().0.unwrap();
9215        let mut frontend = FrontendState::new();
9216        let mock_ctx = ExecutorContext::new_mock(None).await;
9217        let version = Version(0);
9218
9219        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9220        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9221        let sketch_id = sketch_object.id;
9222        let sketch = expect_sketch(sketch_object);
9223        let line_end_id = *sketch.segments.get(1).unwrap();
9224
9225        let constraint = Constraint::Fixed(Fixed {
9226            points: vec![FixedPoint {
9227                point: line_end_id,
9228                position: Point2d {
9229                    x: Number {
9230                        value: 20.0,
9231                        units: NumericSuffix::Mm,
9232                    },
9233                    y: Number {
9234                        value: 0.0,
9235                        units: NumericSuffix::Mm,
9236                    },
9237                },
9238            }],
9239        });
9240        let (constraint_source, _) = frontend
9241            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9242            .await
9243            .unwrap();
9244
9245        assert!(
9246            constraint_source
9247                .text
9248                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9249            "{}",
9250            constraint_source.text
9251        );
9252        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9253        assert_eq!(mock_source.text, constraint_source.text);
9254
9255        mock_ctx.close().await;
9256    }
9257
9258    #[test]
9259    fn test_no_solver_feedback_preserves_original_source() {
9260        let initial_source = "\
9261@settings(defaultLengthUnit = in, kclVersion = 2.0)
9262cylinder = startSketchOn(XY)
9263    |> circle(center= [0, 0], radius= 22)
9264    |> extrude(length = 14)
9265";
9266        let mut frontend = FrontendState::new();
9267        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9268        let outcome = ExecOutcome {
9269            variables: Default::default(),
9270            test_program_memory: Default::default(),
9271            operations: Default::default(),
9272            artifact_graph: Default::default(),
9273            scene_objects: Default::default(),
9274            source_range_to_object: Default::default(),
9275            var_solutions: Default::default(),
9276            refactor_metadata: Default::default(),
9277            issues: Default::default(),
9278            filenames: Default::default(),
9279            source_files: Default::default(),
9280            default_planes: Default::default(),
9281        };
9282
9283        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9284
9285        assert_eq!(source_delta.text, initial_source);
9286    }
9287
9288    /// Explicit drag anchors should limit which edited points become temporary
9289    /// fixed constraints.
9290    #[tokio::test(flavor = "multi_thread")]
9291    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9292        let initial_source = "\
9293sketch(on = XY) {
9294  point1 = point(at = [var 0mm, var 0mm])
9295  point2 = point(at = [var 0mm, var 0mm])
9296  coincident([point1, point2])
9297}
9298";
9299
9300        let program = Program::parse(initial_source).unwrap().0.unwrap();
9301        let mut frontend = FrontendState::new();
9302        let mock_ctx = ExecutorContext::new_mock(None).await;
9303        let version = Version(0);
9304
9305        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9306        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9307        let sketch_id = sketch_object.id;
9308        let sketch = expect_sketch(sketch_object);
9309        let point1_id = sketch.segments[0];
9310        let point2_id = sketch.segments[1];
9311
9312        let segments = vec![
9313            ExistingSegmentCtor {
9314                id: point1_id,
9315                ctor: SegmentCtor::Point(PointCtor {
9316                    position: point_expr_mm(10.0, 0.0),
9317                }),
9318            },
9319            ExistingSegmentCtor {
9320                id: point2_id,
9321                ctor: SegmentCtor::Point(PointCtor {
9322                    position: point_expr_mm(100.0, 0.0),
9323                }),
9324            },
9325        ];
9326        let (_, scene_delta) = frontend
9327            .edit_segments_with_options(
9328                &mock_ctx,
9329                version,
9330                sketch_id,
9331                segments,
9332                EditSegmentsOptions {
9333                    anchor_segment_ids: Some(vec![point1_id]),
9334                    drag_anchors: Vec::new(),
9335                    constraint_label_edits: Vec::new(),
9336                    commit_solved_initial_guesses: true,
9337                },
9338            )
9339            .await
9340            .unwrap();
9341
9342        assert_point_position_close(
9343            point_position(&scene_delta.new_graph, point1_id),
9344            point_number_mm(10.0, 0.0),
9345        );
9346        assert_point_position_close(
9347            point_position(&scene_delta.new_graph, point2_id),
9348            point_number_mm(10.0, 0.0),
9349        );
9350
9351        mock_ctx.close().await;
9352    }
9353
9354    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9355    /// for every SketchVar whose initial NumericLiteral has the given value.
9356    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9357        use std::cell::RefCell;
9358        struct Collector {
9359            target: f64,
9360            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9361        }
9362        impl<'a> crate::walk::Visitor<'a> for &Collector {
9363            type Error = crate::front::Error;
9364            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9365                if let crate::walk::Node::SketchVar(sketch_var) = node
9366                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9367                    && (initial.value - self.target).abs() < 1e-9
9368                {
9369                    self.out
9370                        .borrow_mut()
9371                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9372                }
9373                for child in node.children().iter() {
9374                    if !child.visit(*self)? {
9375                        return Ok(false);
9376                    }
9377                }
9378                Ok(true)
9379            }
9380        }
9381        let collector = Collector {
9382            target: value,
9383            out: Default::default(),
9384        };
9385        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9386        collector.out.into_inner()
9387    }
9388
9389    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9390    /// for every SketchVar (including bare `var`).
9391    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9392        use std::cell::RefCell;
9393        struct Collector {
9394            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9395        }
9396        impl<'a> crate::walk::Visitor<'a> for &Collector {
9397            type Error = crate::front::Error;
9398            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9399                if let crate::walk::Node::SketchVar(sketch_var) = node
9400                    && let Some(node_path) = &sketch_var.node_path
9401                {
9402                    self.out
9403                        .borrow_mut()
9404                        .push((SourceRange::from(sketch_var), node_path.clone()));
9405                }
9406                for child in node.children().iter() {
9407                    if !child.visit(*self)? {
9408                        return Ok(false);
9409                    }
9410                }
9411                Ok(true)
9412            }
9413        }
9414        let collector = Collector {
9415            out: Default::default(),
9416        };
9417        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9418        collector.out.into_inner()
9419    }
9420
9421    fn empty_exec_outcome_with_var_solutions(
9422        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9423    ) -> ExecOutcome {
9424        ExecOutcome {
9425            variables: Default::default(),
9426            test_program_memory: Default::default(),
9427            operations: Default::default(),
9428            artifact_graph: Default::default(),
9429            scene_objects: Default::default(),
9430            source_range_to_object: Default::default(),
9431            var_solutions,
9432            refactor_metadata: Default::default(),
9433            issues: Default::default(),
9434            filenames: Default::default(),
9435            source_files: Default::default(),
9436            default_planes: Default::default(),
9437        }
9438    }
9439
9440    /// Happy path: commit a var solution to a `var N` inside a sketch block
9441    /// using a correct NodePath. Confirms the node-path code path produces the
9442    /// expected source mutation.
9443    #[test]
9444    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9445        let initial_source = "\
9446sketch(on = XY) {
9447  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9448}
9449";
9450        let program = Program::parse(initial_source).unwrap().0.unwrap();
9451        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9452        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9453        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9454
9455        let mut frontend = FrontendState::new();
9456        frontend.program = program;
9457
9458        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9459            literal_range,
9460            Some(node_path),
9461            Number {
9462                value: 25.0,
9463                units: NumericSuffix::Mm,
9464            },
9465        )]);
9466
9467        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9468
9469        insta::assert_snapshot!(
9470            "test_commit_var_solution_by_node_path_updates_sketch_var",
9471            source_delta.text
9472        );
9473    }
9474
9475    /// Whitespace inserted earlier in the source shifts the original SketchVar
9476    /// SourceRange. With NodePath propagation the commit should still target
9477    /// the right `var`. We simulate this by collecting node_paths against a
9478    /// "compact" source, then loading the frontend with a "padded" source
9479    /// (whose byte offsets differ), and feeding the original (now stale)
9480    /// source range plus the correct node_path back into the commit.
9481    #[test]
9482    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9483        let compact_source = "\
9484sketch(on = XY) {
9485  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9486}
9487";
9488        let padded_source = "\
9489// added comment\n// added comment\n\nsketch(on = XY) {
9490  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9491}
9492";
9493        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9494        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9495
9496        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9497            .into_iter()
9498            .next()
9499            .expect("expected `var 10mm` in compact source");
9500        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9501            .into_iter()
9502            .next()
9503            .expect("expected `var 10mm` in padded source");
9504
9505        assert_ne!(
9506            compact_match.0, padded_match.0,
9507            "byte offsets must differ for this test to be meaningful"
9508        );
9509        assert_eq!(
9510            compact_match.1, padded_match.1,
9511            "node paths must agree across whitespace; that's the whole point of NodePath",
9512        );
9513
9514        let mut frontend = FrontendState::new();
9515        frontend.program = padded_program;
9516
9517        // Stale source range from the compact source + correct node_path.
9518        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9519            compact_match.0,
9520            Some(compact_match.1),
9521            Number {
9522                value: 30.0,
9523                units: NumericSuffix::Mm,
9524            },
9525        )]);
9526
9527        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9528
9529        insta::assert_snapshot!(
9530            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9531            source_delta.text
9532        );
9533    }
9534
9535    /// When multiple `var` declarations exist and the stale source range
9536    /// happens to land on a *different* var, the node_path must take
9537    /// precedence and the right var gets updated.
9538    #[test]
9539    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9540        let initial_source = "\
9541sketch(on = XY) {
9542  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9543}
9544";
9545        let program = Program::parse(initial_source).unwrap().0.unwrap();
9546
9547        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9548            .into_iter()
9549            .next()
9550            .expect("expected `var 10mm`");
9551        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9552            .into_iter()
9553            .next()
9554            .expect("expected `var 20mm`");
9555
9556        let mut frontend = FrontendState::new();
9557        frontend.program = program;
9558
9559        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9560        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9561            var_20.0,
9562            Some(var_10.1),
9563            Number {
9564                value: 33.0,
9565                units: NumericSuffix::Mm,
9566            },
9567        )]);
9568
9569        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9570
9571        insta::assert_snapshot!(
9572            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9573            source_delta.text
9574        );
9575    }
9576
9577    /// Bare `var` (no initial literal) is only locatable via node_path. With
9578    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9579    /// solver solution should fill the initial value in. The
9580    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9581    /// is gated as an experimental feature; without it the re-parse of the
9582    /// recast source rejects bare `var` declarations.
9583    #[test]
9584    fn test_commit_var_solution_writes_back_into_bare_var() {
9585        let initial_source = "\
9586@settings(experimentalFeatures = allow, kclVersion = 2.0)
9587sketch(on = XY) {
9588  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9589}
9590";
9591        let program = Program::parse(initial_source).unwrap().0.unwrap();
9592
9593        // Pick the first bare `var`; collect_all_sketch_vars returns every
9594        // SketchVar, including bare ones.
9595        let bare = collect_all_sketch_vars(&program)
9596            .into_iter()
9597            .find(|(range, _)| {
9598                // The bare `var` is exactly the 3 characters "var".
9599                range.end() - range.start() == 3
9600            })
9601            .expect("expected at least one bare `var`");
9602
9603        let mut frontend = FrontendState::new();
9604        frontend.program = program;
9605
9606        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9607            bare.0,
9608            Some(bare.1),
9609            Number {
9610                value: 7.0,
9611                units: NumericSuffix::Mm,
9612            },
9613        )]);
9614
9615        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9616
9617        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9618        // written as an explicit suffix so the bare var commits with units.
9619        // The recast adds a blank line after the `@settings` annotation.
9620        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9621    }
9622
9623    #[tokio::test(flavor = "multi_thread")]
9624    async fn test_delete_point_without_var() {
9625        let initial_source = "\
9626sketch(on = XY) {
9627  point(at = [var 1, var 2])
9628  point(at = [var 3, var 4])
9629  point(at = [var 5, var 6])
9630}
9631";
9632
9633        let program = Program::parse(initial_source).unwrap().0.unwrap();
9634
9635        let mut frontend = FrontendState::new();
9636
9637        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9638        let mock_ctx = ExecutorContext::new_mock(None).await;
9639        let version = Version(0);
9640
9641        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9642        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9643        let sketch_id = sketch_object.id;
9644        let sketch = expect_sketch(sketch_object);
9645
9646        let point_id = *sketch.segments.get(1).unwrap();
9647
9648        let (src_delta, scene_delta) = frontend
9649            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9650            .await
9651            .unwrap();
9652        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9653        assert_eq!(scene_delta.new_objects, vec![]);
9654        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9655
9656        ctx.close().await;
9657        mock_ctx.close().await;
9658    }
9659
9660    #[tokio::test(flavor = "multi_thread")]
9661    async fn test_delete_point_with_var() {
9662        let initial_source = "\
9663sketch(on = XY) {
9664  point(at = [var 1, var 2])
9665  point1 = point(at = [var 3, var 4])
9666  point(at = [var 5, var 6])
9667}
9668";
9669
9670        let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672        let mut frontend = FrontendState::new();
9673
9674        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9675        let mock_ctx = ExecutorContext::new_mock(None).await;
9676        let version = Version(0);
9677
9678        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9679        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9680        let sketch_id = sketch_object.id;
9681        let sketch = expect_sketch(sketch_object);
9682
9683        let point_id = *sketch.segments.get(1).unwrap();
9684
9685        let (src_delta, scene_delta) = frontend
9686            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9687            .await
9688            .unwrap();
9689        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9690        assert_eq!(scene_delta.new_objects, vec![]);
9691        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9692
9693        ctx.close().await;
9694        mock_ctx.close().await;
9695    }
9696
9697    #[tokio::test(flavor = "multi_thread")]
9698    async fn test_delete_multiple_points() {
9699        let initial_source = "\
9700sketch(on = XY) {
9701  point(at = [var 1, var 2])
9702  point1 = point(at = [var 3, var 4])
9703  point(at = [var 5, var 6])
9704}
9705";
9706
9707        let program = Program::parse(initial_source).unwrap().0.unwrap();
9708
9709        let mut frontend = FrontendState::new();
9710
9711        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9712        let mock_ctx = ExecutorContext::new_mock(None).await;
9713        let version = Version(0);
9714
9715        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9716        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9717        let sketch_id = sketch_object.id;
9718
9719        let sketch = expect_sketch(sketch_object);
9720
9721        let point1_id = *sketch.segments.first().unwrap();
9722        let point2_id = *sketch.segments.get(1).unwrap();
9723
9724        let (src_delta, scene_delta) = frontend
9725            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9726            .await
9727            .unwrap();
9728        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9729        assert_eq!(scene_delta.new_objects, vec![]);
9730        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9731
9732        ctx.close().await;
9733        mock_ctx.close().await;
9734    }
9735
9736    #[tokio::test(flavor = "multi_thread")]
9737    async fn test_delete_coincident_constraint() {
9738        let initial_source = "\
9739sketch(on = XY) {
9740  point1 = point(at = [var 1, var 2])
9741  point2 = point(at = [var 3, var 4])
9742  coincident([point1, point2])
9743  point(at = [var 5, var 6])
9744}
9745";
9746
9747        let program = Program::parse(initial_source).unwrap().0.unwrap();
9748
9749        let mut frontend = FrontendState::new();
9750
9751        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9752        let mock_ctx = ExecutorContext::new_mock(None).await;
9753        let version = Version(0);
9754
9755        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9756        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9757        let sketch_id = sketch_object.id;
9758        let sketch = expect_sketch(sketch_object);
9759
9760        let coincident_id = *sketch.constraints.first().unwrap();
9761
9762        let (src_delta, scene_delta) = frontend
9763            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9764            .await
9765            .unwrap();
9766        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9767        assert_eq!(scene_delta.new_objects, vec![]);
9768        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9769
9770        ctx.close().await;
9771        mock_ctx.close().await;
9772    }
9773
9774    #[tokio::test(flavor = "multi_thread")]
9775    async fn test_delete_line_cascades_to_coincident_constraint() {
9776        let initial_source = "\
9777sketch(on = XY) {
9778  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9779  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9780  coincident([line1.end, line2.start])
9781}
9782";
9783
9784        let program = Program::parse(initial_source).unwrap().0.unwrap();
9785
9786        let mut frontend = FrontendState::new();
9787
9788        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9789        let mock_ctx = ExecutorContext::new_mock(None).await;
9790        let version = Version(0);
9791
9792        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9793        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9794        let sketch_id = sketch_object.id;
9795        let sketch = expect_sketch(sketch_object);
9796        let line_id = *sketch.segments.get(5).unwrap();
9797
9798        let (src_delta, scene_delta) = frontend
9799            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9800            .await
9801            .unwrap();
9802        insta::assert_snapshot!(
9803            "test_delete_line_cascades_to_coincident_constraint",
9804            src_delta.text.as_str()
9805        );
9806        assert_eq!(
9807            scene_delta.new_graph.objects.len(),
9808            5,
9809            "{:#?}",
9810            scene_delta.new_graph.objects
9811        );
9812
9813        ctx.close().await;
9814        mock_ctx.close().await;
9815    }
9816
9817    #[tokio::test(flavor = "multi_thread")]
9818    async fn test_delete_line_cascades_to_distance_constraint() {
9819        let initial_source = "\
9820sketch(on = XY) {
9821  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9822  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9823  distance([line1.end, line2.start]) == 10mm
9824}
9825";
9826
9827        let program = Program::parse(initial_source).unwrap().0.unwrap();
9828
9829        let mut frontend = FrontendState::new();
9830
9831        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9832        let mock_ctx = ExecutorContext::new_mock(None).await;
9833        let version = Version(0);
9834
9835        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9836        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837        let sketch_id = sketch_object.id;
9838        let sketch = expect_sketch(sketch_object);
9839        let line_id = *sketch.segments.get(5).unwrap();
9840
9841        let (src_delta, scene_delta) = frontend
9842            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9843            .await
9844            .unwrap();
9845        insta::assert_snapshot!(
9846            "test_delete_line_cascades_to_distance_constraint",
9847            src_delta.text.as_str()
9848        );
9849        assert_eq!(
9850            scene_delta.new_graph.objects.len(),
9851            5,
9852            "{:#?}",
9853            scene_delta.new_graph.objects
9854        );
9855
9856        ctx.close().await;
9857        mock_ctx.close().await;
9858    }
9859
9860    #[tokio::test(flavor = "multi_thread")]
9861    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9862        let initial_source = "\
9863sketch(on = XY) {
9864  point1 = point(at = [var 1, var 2])
9865  point2 = point(at = [var 3, var 4])
9866  horizontalDistance([point1, point2]) == 10mm
9867}
9868";
9869
9870        let program = Program::parse(initial_source).unwrap().0.unwrap();
9871
9872        let mut frontend = FrontendState::new();
9873
9874        let mock_ctx = ExecutorContext::new_mock(None).await;
9875        let version = Version(0);
9876
9877        frontend.program = program.clone();
9878        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9879        frontend.update_state_after_exec(outcome, true);
9880        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9881        let sketch_id = sketch_object.id;
9882        let sketch = expect_sketch(sketch_object);
9883        let point2_id = *sketch.segments.get(1).unwrap();
9884
9885        let (src_delta, scene_delta) = frontend
9886            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9887            .await
9888            .unwrap();
9889        insta::assert_snapshot!(
9890            "test_delete_point_cascades_to_horizontal_distance_constraint",
9891            src_delta.text.as_str()
9892        );
9893        assert_eq!(
9894            scene_delta.new_graph.objects.len(),
9895            3,
9896            "{:#?}",
9897            scene_delta.new_graph.objects
9898        );
9899
9900        mock_ctx.close().await;
9901    }
9902
9903    #[tokio::test(flavor = "multi_thread")]
9904    async fn test_delete_line_cascades_to_fixed_constraint() {
9905        let initial_source = "\
9906sketch(on = XY) {
9907  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9908  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9909  fixed([line1.start, [0, 0]])
9910}
9911";
9912
9913        let program = Program::parse(initial_source).unwrap().0.unwrap();
9914
9915        let mut frontend = FrontendState::new();
9916
9917        let mock_ctx = ExecutorContext::new_mock(None).await;
9918        let version = Version(0);
9919
9920        frontend.program = program.clone();
9921        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9922        frontend.update_state_after_exec(outcome, true);
9923        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9924        let sketch_id = sketch_object.id;
9925        let sketch = expect_sketch(sketch_object);
9926        let line1_id = *sketch.segments.get(2).unwrap();
9927
9928        let (src_delta, scene_delta) = frontend
9929            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9930            .await
9931            .unwrap();
9932        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9933        assert_eq!(
9934            scene_delta.new_graph.objects.len(),
9935            5,
9936            "{:#?}",
9937            scene_delta.new_graph.objects
9938        );
9939
9940        mock_ctx.close().await;
9941    }
9942
9943    #[tokio::test(flavor = "multi_thread")]
9944    async fn test_delete_line_cascades_to_midpoint_constraint() {
9945        let initial_source = "\
9946sketch(on = XY) {
9947  point1 = point(at = [var 1, var 2])
9948  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9949  midpoint(line1, point = point1)
9950}
9951";
9952
9953        let program = Program::parse(initial_source).unwrap().0.unwrap();
9954
9955        let mut frontend = FrontendState::new();
9956
9957        let mock_ctx = ExecutorContext::new_mock(None).await;
9958        let version = Version(0);
9959
9960        frontend.program = program.clone();
9961        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9962        frontend.update_state_after_exec(outcome, true);
9963        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9964        let sketch_id = sketch_object.id;
9965        let sketch = expect_sketch(sketch_object);
9966        let line1_id = *sketch.segments.get(3).unwrap();
9967
9968        let (src_delta, scene_delta) = frontend
9969            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9970            .await
9971            .unwrap();
9972        insta::assert_snapshot!(
9973            "test_delete_line_cascades_to_midpoint_constraint",
9974            src_delta.text.as_str()
9975        );
9976        assert_eq!(
9977            scene_delta.new_graph.objects.len(),
9978            3,
9979            "{:#?}",
9980            scene_delta.new_graph.objects
9981        );
9982
9983        mock_ctx.close().await;
9984    }
9985
9986    #[tokio::test(flavor = "multi_thread")]
9987    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9988        let initial_source = "\
9989sketch(on = XY) {
9990  point1 = point(at = [var 1, var 2])
9991  point2 = point(at = [var 3, var 4])
9992  point3 = point(at = [var 5, var 6])
9993  coincident([point1, point2, point3])
9994}
9995";
9996
9997        let program = Program::parse(initial_source).unwrap().0.unwrap();
9998
9999        let mut frontend = FrontendState::new();
10000
10001        let mock_ctx = ExecutorContext::new_mock(None).await;
10002        let version = Version(0);
10003
10004        frontend.program = program.clone();
10005        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10006        frontend.update_state_after_exec(outcome, true);
10007        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10008        let sketch_id = sketch_object.id;
10009        let sketch = expect_sketch(sketch_object);
10010        let point3_id = *sketch.segments.get(2).unwrap();
10011
10012        let (src_delta, scene_delta) = frontend
10013            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10014            .await
10015            .unwrap();
10016        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10017        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10018        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10019        assert!(
10020            src_delta.text.contains("coincident([point1, point2])"),
10021            "{}",
10022            src_delta.text
10023        );
10024
10025        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10026        let sketch = expect_sketch(sketch_object);
10027        assert_eq!(sketch.segments.len(), 2);
10028        assert_eq!(sketch.constraints.len(), 1);
10029
10030        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10031        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10032            panic!("Expected constraint object");
10033        };
10034        let Constraint::Coincident(coincident) = constraint else {
10035            panic!("Expected coincident constraint");
10036        };
10037        assert_eq!(
10038            coincident.segments,
10039            sketch
10040                .segments
10041                .iter()
10042                .copied()
10043                .map(Into::into)
10044                .collect::<Vec<ConstraintSegment>>()
10045        );
10046
10047        mock_ctx.close().await;
10048    }
10049
10050    #[tokio::test(flavor = "multi_thread")]
10051    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10052        let initial_source = "\
10053sketch(on = XY) {
10054  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10055  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10056  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10057  equalLength([line1, line2, line3])
10058}
10059";
10060
10061        let program = Program::parse(initial_source).unwrap().0.unwrap();
10062
10063        let mut frontend = FrontendState::new();
10064
10065        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10066        let mock_ctx = ExecutorContext::new_mock(None).await;
10067        let version = Version(0);
10068
10069        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10070        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10071        let sketch_id = sketch_object.id;
10072        let sketch = expect_sketch(sketch_object);
10073        let line3_id = *sketch.segments.get(8).unwrap();
10074
10075        let (src_delta, scene_delta) = frontend
10076            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10077            .await
10078            .unwrap();
10079        insta::assert_snapshot!(
10080            "test_delete_line_preserves_multiline_equal_length_constraint",
10081            src_delta.text.as_str()
10082        );
10083
10084        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10085        let sketch = expect_sketch(sketch_object);
10086        assert_eq!(sketch.constraints.len(), 1);
10087
10088        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10089        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10090            panic!("Expected constraint object");
10091        };
10092        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10093            panic!("Expected lines equal length constraint");
10094        };
10095        assert_eq!(lines_equal_length.lines.len(), 2);
10096
10097        ctx.close().await;
10098        mock_ctx.close().await;
10099    }
10100
10101    #[tokio::test(flavor = "multi_thread")]
10102    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10103        let initial_source = "\
10104sketch(on = XY) {
10105  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10106  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10107  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10108  horizontal([line1.end, line2.start, line3.start])
10109}
10110";
10111
10112        let program = Program::parse(initial_source).unwrap().0.unwrap();
10113
10114        let mut frontend = FrontendState::new();
10115
10116        let mock_ctx = ExecutorContext::new_mock(None).await;
10117        let version = Version(0);
10118
10119        frontend.program = program.clone();
10120        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10121        frontend.update_state_after_exec(outcome, true);
10122        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10123        let sketch_id = sketch_object.id;
10124        let sketch = expect_sketch(sketch_object);
10125        let line1_id = *sketch.segments.get(2).unwrap();
10126
10127        let (src_delta, scene_delta) = frontend
10128            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10129            .await
10130            .unwrap();
10131        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10132        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10133        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10134        assert!(
10135            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10136            "{}",
10137            src_delta.text
10138        );
10139
10140        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10141        let sketch = expect_sketch(sketch_object);
10142        assert_eq!(sketch.constraints.len(), 1);
10143
10144        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10145        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10146            panic!("Expected constraint object");
10147        };
10148        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10149            panic!("Expected horizontal points constraint");
10150        };
10151        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10152        assert_eq!(*points, remaining_points);
10153
10154        mock_ctx.close().await;
10155    }
10156
10157    #[tokio::test(flavor = "multi_thread")]
10158    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10159        let initial_source = "\
10160sketch(on = XY) {
10161  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10162  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10163  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10164  vertical([line1.end, line2.start, line3.start])
10165}
10166";
10167
10168        let program = Program::parse(initial_source).unwrap().0.unwrap();
10169
10170        let mut frontend = FrontendState::new();
10171
10172        let mock_ctx = ExecutorContext::new_mock(None).await;
10173        let version = Version(0);
10174
10175        frontend.program = program.clone();
10176        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10177        frontend.update_state_after_exec(outcome, true);
10178        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10179        let sketch_id = sketch_object.id;
10180        let sketch = expect_sketch(sketch_object);
10181        let line1_id = *sketch.segments.get(2).unwrap();
10182
10183        let (src_delta, scene_delta) = frontend
10184            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10185            .await
10186            .unwrap();
10187        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10188        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10189        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10190        assert!(
10191            src_delta.text.contains("vertical([line2.start, line3.start])"),
10192            "{}",
10193            src_delta.text
10194        );
10195
10196        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10197        let sketch = expect_sketch(sketch_object);
10198        assert_eq!(sketch.constraints.len(), 1);
10199
10200        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10201        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10202            panic!("Expected constraint object");
10203        };
10204        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10205            panic!("Expected vertical points constraint");
10206        };
10207        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10208        assert_eq!(*points, remaining_points);
10209
10210        mock_ctx.close().await;
10211    }
10212
10213    #[tokio::test(flavor = "multi_thread")]
10214    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10215        let initial_source = "\
10216sketch(on = XY) {
10217  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10218  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10219  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10220  coincident([line1.end, line2.start, line3.start])
10221}
10222";
10223
10224        let program = Program::parse(initial_source).unwrap().0.unwrap();
10225
10226        let mut frontend = FrontendState::new();
10227
10228        let mock_ctx = ExecutorContext::new_mock(None).await;
10229        let version = Version(0);
10230
10231        frontend.program = program.clone();
10232        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10233        frontend.update_state_after_exec(outcome, true);
10234        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10235        let sketch_id = sketch_object.id;
10236        let sketch = expect_sketch(sketch_object);
10237        let line1_id = *sketch.segments.get(2).unwrap();
10238
10239        let (src_delta, scene_delta) = frontend
10240            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10241            .await
10242            .unwrap();
10243        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10244        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10245        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10246        assert!(
10247            src_delta.text.contains("coincident([line2.start, line3.start])"),
10248            "{}",
10249            src_delta.text
10250        );
10251
10252        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10253        let sketch = expect_sketch(sketch_object);
10254        assert_eq!(sketch.constraints.len(), 1);
10255
10256        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10257        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10258            panic!("Expected constraint object");
10259        };
10260        let Constraint::Coincident(coincident) = constraint else {
10261            panic!("Expected coincident constraint");
10262        };
10263        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10264        assert_eq!(coincident.segments, remaining_segments);
10265
10266        mock_ctx.close().await;
10267    }
10268
10269    #[tokio::test(flavor = "multi_thread")]
10270    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10271        let initial_source = "\
10272sketch(on = XY) {
10273  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10274  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10275  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10276  equalLength([line1, line2, line3])
10277}
10278";
10279
10280        let program = Program::parse(initial_source).unwrap().0.unwrap();
10281
10282        let mut frontend = FrontendState::new();
10283
10284        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10285        let mock_ctx = ExecutorContext::new_mock(None).await;
10286        let version = Version(0);
10287
10288        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10289        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10290        let sketch_id = sketch_object.id;
10291        let sketch = expect_sketch(sketch_object);
10292        let line2_id = *sketch.segments.get(5).unwrap();
10293        let line3_id = *sketch.segments.get(8).unwrap();
10294
10295        let (src_delta, scene_delta) = frontend
10296            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10297            .await
10298            .unwrap();
10299        insta::assert_snapshot!(
10300            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10301            src_delta.text.as_str()
10302        );
10303
10304        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10305        let sketch = expect_sketch(sketch_object);
10306        assert!(sketch.constraints.is_empty());
10307
10308        ctx.close().await;
10309        mock_ctx.close().await;
10310    }
10311
10312    #[tokio::test(flavor = "multi_thread")]
10313    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10314        let initial_source = "\
10315sketch(on = XY) {
10316  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10317  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10318  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10319  parallel([line1, line2, line3])
10320}
10321";
10322
10323        let program = Program::parse(initial_source).unwrap().0.unwrap();
10324
10325        let mut frontend = FrontendState::new();
10326
10327        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10328        let mock_ctx = ExecutorContext::new_mock(None).await;
10329        let version = Version(0);
10330
10331        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10332        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10333        let sketch_id = sketch_object.id;
10334        let sketch = expect_sketch(sketch_object);
10335        let line3_id = *sketch.segments.get(8).unwrap();
10336
10337        let (src_delta, scene_delta) = frontend
10338            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10339            .await
10340            .unwrap();
10341        insta::assert_snapshot!(
10342            "test_delete_line_preserves_multiline_parallel_constraint",
10343            src_delta.text.as_str()
10344        );
10345
10346        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10347        let sketch = expect_sketch(sketch_object);
10348        assert_eq!(sketch.constraints.len(), 1);
10349
10350        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10351        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10352            panic!("Expected constraint object");
10353        };
10354        let Constraint::Parallel(parallel) = constraint else {
10355            panic!("Expected parallel constraint");
10356        };
10357        assert_eq!(parallel.lines.len(), 2);
10358
10359        ctx.close().await;
10360        mock_ctx.close().await;
10361    }
10362
10363    #[tokio::test(flavor = "multi_thread")]
10364    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10365        let initial_source = "\
10366sketch(on = XY) {
10367  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10368  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10369  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10370  parallel([line1, line2, line3])
10371}
10372";
10373
10374        let program = Program::parse(initial_source).unwrap().0.unwrap();
10375
10376        let mut frontend = FrontendState::new();
10377
10378        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10379        let mock_ctx = ExecutorContext::new_mock(None).await;
10380        let version = Version(0);
10381
10382        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10383        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10384        let sketch_id = sketch_object.id;
10385        let sketch = expect_sketch(sketch_object);
10386        let line2_id = *sketch.segments.get(5).unwrap();
10387        let line3_id = *sketch.segments.get(8).unwrap();
10388
10389        let (src_delta, scene_delta) = frontend
10390            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10391            .await
10392            .unwrap();
10393        insta::assert_snapshot!(
10394            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10395            src_delta.text.as_str()
10396        );
10397
10398        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10399        let sketch = expect_sketch(sketch_object);
10400        assert!(sketch.constraints.is_empty());
10401
10402        ctx.close().await;
10403        mock_ctx.close().await;
10404    }
10405
10406    #[tokio::test(flavor = "multi_thread")]
10407    async fn test_delete_line_line_coincident_constraint() {
10408        let initial_source = "\
10409sketch(on = XY) {
10410  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10411  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10412  coincident([line1, line2])
10413}
10414";
10415
10416        let program = Program::parse(initial_source).unwrap().0.unwrap();
10417
10418        let mut frontend = FrontendState::new();
10419
10420        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10421        let mock_ctx = ExecutorContext::new_mock(None).await;
10422        let version = Version(0);
10423
10424        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10425        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10426        let sketch_id = sketch_object.id;
10427        let sketch = expect_sketch(sketch_object);
10428
10429        let coincident_id = *sketch.constraints.first().unwrap();
10430
10431        let (src_delta, scene_delta) = frontend
10432            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10433            .await
10434            .unwrap();
10435        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10436        assert_eq!(scene_delta.new_objects, vec![]);
10437        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10438
10439        ctx.close().await;
10440        mock_ctx.close().await;
10441    }
10442
10443    #[tokio::test(flavor = "multi_thread")]
10444    async fn test_two_points_coincident() {
10445        let initial_source = "\
10446sketch(on = XY) {
10447  point1 = point(at = [var 1, var 2])
10448  point(at = [3, 4])
10449}
10450";
10451
10452        let program = Program::parse(initial_source).unwrap().0.unwrap();
10453
10454        let mut frontend = FrontendState::new();
10455
10456        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10457        let mock_ctx = ExecutorContext::new_mock(None).await;
10458        let version = Version(0);
10459
10460        frontend.hack_set_program(&ctx, program).await.unwrap();
10461        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10462        let sketch_id = sketch_object.id;
10463        let sketch = expect_sketch(sketch_object);
10464        let point0_id = *sketch.segments.first().unwrap();
10465        let point1_id = *sketch.segments.get(1).unwrap();
10466
10467        let constraint = Constraint::Coincident(Coincident {
10468            segments: vec![point0_id.into(), point1_id.into()],
10469        });
10470        let (src_delta, scene_delta) = frontend
10471            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10472            .await
10473            .unwrap();
10474        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10475        assert_eq!(
10476            scene_delta.new_graph.objects.len(),
10477            5,
10478            "{:#?}",
10479            scene_delta.new_graph.objects
10480        );
10481
10482        ctx.close().await;
10483        mock_ctx.close().await;
10484    }
10485
10486    #[tokio::test(flavor = "multi_thread")]
10487    async fn test_three_points_coincident() {
10488        let initial_source = "\
10489sketch(on = XY) {
10490  point1 = point(at = [var 1, var 2])
10491  point(at = [var 3, var 4])
10492  point(at = [var 5, var 6])
10493}
10494";
10495
10496        let program = Program::parse(initial_source).unwrap().0.unwrap();
10497
10498        let mut frontend = FrontendState::new();
10499
10500        let mock_ctx = ExecutorContext::new_mock(None).await;
10501        let version = Version(0);
10502
10503        frontend.program = program.clone();
10504        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10505        frontend.update_state_after_exec(outcome, true);
10506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10507        let sketch_id = sketch_object.id;
10508        let sketch = expect_sketch(sketch_object);
10509        let segments = sketch
10510            .segments
10511            .iter()
10512            .take(3)
10513            .copied()
10514            .map(Into::into)
10515            .collect::<Vec<ConstraintSegment>>();
10516
10517        let constraint = Constraint::Coincident(Coincident {
10518            segments: segments.clone(),
10519        });
10520        let (src_delta, scene_delta) = frontend
10521            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10522            .await
10523            .unwrap();
10524        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10525
10526        let constraint_object = scene_delta
10527            .new_graph
10528            .objects
10529            .iter()
10530            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10531            .unwrap();
10532
10533        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10534            panic!("expected a constraint object");
10535        };
10536
10537        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10538
10539        mock_ctx.close().await;
10540    }
10541
10542    #[tokio::test(flavor = "multi_thread")]
10543    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10544        let initial_source = "\
10545sketch(on = XY) {
10546  point1 = point(at = [var 1, var 2])
10547  point2 = point(at = [var 3, var 4])
10548  point3 = point(at = [var 5, var 6])
10549  coincident([point1, point2, point3])
10550}
10551";
10552
10553        let program = Program::parse(initial_source).unwrap().0.unwrap();
10554
10555        let mut frontend = FrontendState::new();
10556
10557        let ctx = ExecutorContext::new_mock(None).await;
10558        frontend.program = program.clone();
10559        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10560        frontend.update_state_after_exec(outcome, true);
10561
10562        let constraint_object = frontend
10563            .scene_graph
10564            .objects
10565            .iter()
10566            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10567            .unwrap();
10568        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10569            panic!("expected a constraint object");
10570        };
10571
10572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10573        let sketch = expect_sketch(sketch_object);
10574        let expected_segments = sketch
10575            .segments
10576            .iter()
10577            .take(3)
10578            .copied()
10579            .map(Into::into)
10580            .collect::<Vec<ConstraintSegment>>();
10581
10582        assert_eq!(
10583            constraint,
10584            &Constraint::Coincident(Coincident {
10585                segments: expected_segments,
10586            })
10587        );
10588
10589        ctx.close().await;
10590    }
10591
10592    #[tokio::test(flavor = "multi_thread")]
10593    async fn test_point_origin_coincident_preserves_order() {
10594        let initial_source = "\
10595sketch(on = XY) {
10596  point(at = [var 1, var 2])
10597}
10598";
10599
10600        for (origin_first, snapshot_name) in [
10601            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10602            (false, "test_point_origin_coincident_preserves_order_point_first"),
10603        ] {
10604            let program = Program::parse(initial_source).unwrap().0.unwrap();
10605
10606            let mut frontend = FrontendState::new();
10607
10608            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10609            let mock_ctx = ExecutorContext::new_mock(None).await;
10610            let version = Version(0);
10611
10612            frontend.hack_set_program(&ctx, program).await.unwrap();
10613            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10614            let sketch_id = sketch_object.id;
10615            let sketch = expect_sketch(sketch_object);
10616            let point_id = *sketch.segments.first().unwrap();
10617
10618            let segments = if origin_first {
10619                vec![ConstraintSegment::ORIGIN, point_id.into()]
10620            } else {
10621                vec![point_id.into(), ConstraintSegment::ORIGIN]
10622            };
10623            let constraint = Constraint::Coincident(Coincident {
10624                segments: segments.clone(),
10625            });
10626            let (src_delta, scene_delta) = frontend
10627                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10628                .await
10629                .unwrap();
10630            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10631
10632            let constraint_object = scene_delta
10633                .new_graph
10634                .objects
10635                .iter()
10636                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10637                .unwrap();
10638
10639            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10640                panic!("expected a constraint object");
10641            };
10642
10643            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10644
10645            ctx.close().await;
10646            mock_ctx.close().await;
10647        }
10648    }
10649
10650    #[tokio::test(flavor = "multi_thread")]
10651    async fn test_coincident_of_line_end_points() {
10652        let initial_source = "\
10653sketch(on = XY) {
10654  line(start = [var 1, var 2], end = [var 3, var 4])
10655  line(start = [var 5, var 6], end = [var 7, var 8])
10656}
10657";
10658
10659        let program = Program::parse(initial_source).unwrap().0.unwrap();
10660
10661        let mut frontend = FrontendState::new();
10662
10663        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10664        let mock_ctx = ExecutorContext::new_mock(None).await;
10665        let version = Version(0);
10666
10667        frontend.hack_set_program(&ctx, program).await.unwrap();
10668        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10669        let sketch_id = sketch_object.id;
10670        let sketch = expect_sketch(sketch_object);
10671        let point0_id = *sketch.segments.get(1).unwrap();
10672        let point1_id = *sketch.segments.get(3).unwrap();
10673
10674        let constraint = Constraint::Coincident(Coincident {
10675            segments: vec![point0_id.into(), point1_id.into()],
10676        });
10677        let (src_delta, scene_delta) = frontend
10678            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10679            .await
10680            .unwrap();
10681        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10682        assert_eq!(
10683            scene_delta.new_graph.objects.len(),
10684            9,
10685            "{:#?}",
10686            scene_delta.new_graph.objects
10687        );
10688
10689        ctx.close().await;
10690        mock_ctx.close().await;
10691    }
10692
10693    #[tokio::test(flavor = "multi_thread")]
10694    async fn test_coincident_of_line_point_and_circle_segment() {
10695        let initial_source = "\
10696sketch(on = XY) {
10697  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10698  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10699}
10700";
10701        let program = Program::parse(initial_source).unwrap().0.unwrap();
10702        let mut frontend = FrontendState::new();
10703
10704        let mock_ctx = ExecutorContext::new_mock(None).await;
10705        let version = Version(0);
10706
10707        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10708        frontend.program = program;
10709        frontend.update_state_after_exec(outcome, true);
10710        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10711        let sketch_id = sketch_object.id;
10712        let sketch = expect_sketch(sketch_object);
10713
10714        let circle_id = sketch
10715            .segments
10716            .iter()
10717            .copied()
10718            .find(|seg_id| {
10719                matches!(
10720                    &frontend.scene_graph.objects[seg_id.0].kind,
10721                    ObjectKind::Segment {
10722                        segment: Segment::Circle(_)
10723                    }
10724                )
10725            })
10726            .expect("Expected a circle segment in sketch");
10727        let line_id = frontend
10728            .scene_graph
10729            .objects
10730            .iter()
10731            .find_map(|obj| match &obj.kind {
10732                ObjectKind::Segment {
10733                    segment: Segment::Line(line),
10734                } if line.owner.is_none() => Some(obj.id),
10735                _ => None,
10736            })
10737            .expect("Expected a standalone line segment in scene graph");
10738
10739        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10740            ObjectKind::Segment {
10741                segment: Segment::Line(line),
10742            } => line.start,
10743            _ => panic!("Expected line segment object"),
10744        };
10745
10746        let constraint = Constraint::Coincident(Coincident {
10747            segments: vec![line_start_point_id.into(), circle_id.into()],
10748        });
10749        let (src_delta, _scene_delta) = frontend
10750            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10751            .await
10752            .unwrap();
10753        insta::assert_snapshot!(
10754            "test_coincident_of_line_point_and_circle_segment",
10755            src_delta.text.as_str()
10756        );
10757
10758        mock_ctx.close().await;
10759    }
10760
10761    #[tokio::test(flavor = "multi_thread")]
10762    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10763        // Test that attempting an invalid coincident constraint (arc and line)
10764        // doesn't corrupt the state, allowing subsequent operations to work.
10765        // This test verifies the transactional fix in add_constraint that prevents
10766        // state corruption when invalid constraints are attempted.
10767        // Example: coincident constraint between an arc segment and a straight line segment
10768        // is geometrically invalid and should fail, but state should remain intact.
10769        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10770        let program = Program::empty();
10771
10772        let mut frontend = FrontendState::new();
10773        frontend.program = program;
10774
10775        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10776        let mock_ctx = ExecutorContext::new_mock(None).await;
10777        let version = Version(0);
10778
10779        let sketch_args = SketchCtor {
10780            on: Plane::Default(PlaneName::Xy),
10781        };
10782        let (_src_delta, _scene_delta, sketch_id) = frontend
10783            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10784            .await
10785            .unwrap();
10786
10787        // Add an arc segment
10788        let arc_ctor = ArcCtor {
10789            start: Point2d {
10790                x: Expr::Var(Number {
10791                    value: 0.0,
10792                    units: NumericSuffix::Mm,
10793                }),
10794                y: Expr::Var(Number {
10795                    value: 0.0,
10796                    units: NumericSuffix::Mm,
10797                }),
10798            },
10799            end: Point2d {
10800                x: Expr::Var(Number {
10801                    value: 10.0,
10802                    units: NumericSuffix::Mm,
10803                }),
10804                y: Expr::Var(Number {
10805                    value: 10.0,
10806                    units: NumericSuffix::Mm,
10807                }),
10808            },
10809            center: Point2d {
10810                x: Expr::Var(Number {
10811                    value: 10.0,
10812                    units: NumericSuffix::Mm,
10813                }),
10814                y: Expr::Var(Number {
10815                    value: 0.0,
10816                    units: NumericSuffix::Mm,
10817                }),
10818            },
10819            direction: None,
10820            construction: None,
10821        };
10822        let (_src_delta, scene_delta) = frontend
10823            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10824            .await
10825            .unwrap();
10826        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10827        let arc_id = *scene_delta.new_objects.last().unwrap();
10828
10829        // Add a line segment
10830        let line_ctor = LineCtor {
10831            start: Point2d {
10832                x: Expr::Var(Number {
10833                    value: 20.0,
10834                    units: NumericSuffix::Mm,
10835                }),
10836                y: Expr::Var(Number {
10837                    value: 0.0,
10838                    units: NumericSuffix::Mm,
10839                }),
10840            },
10841            end: Point2d {
10842                x: Expr::Var(Number {
10843                    value: 30.0,
10844                    units: NumericSuffix::Mm,
10845                }),
10846                y: Expr::Var(Number {
10847                    value: 10.0,
10848                    units: NumericSuffix::Mm,
10849                }),
10850            },
10851            construction: None,
10852        };
10853        let (_src_delta, scene_delta) = frontend
10854            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10855            .await
10856            .unwrap();
10857        // The line is the last object in new_objects (after the 2 points: start, end)
10858        let line_id = *scene_delta.new_objects.last().unwrap();
10859
10860        // Attempt to add an invalid coincident constraint between arc and line
10861        // This should fail during execution, but state should remain intact
10862        let constraint = Constraint::Coincident(Coincident {
10863            segments: vec![arc_id.into(), line_id.into()],
10864        });
10865        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10866
10867        // The constraint addition should fail (invalid constraint)
10868        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10869
10870        // Verify state is not corrupted by checking that we can still access the scene graph
10871        // and that the original segments are still present with their source ranges
10872        let sketch_object_after =
10873            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10874        let sketch_after = expect_sketch(sketch_object_after);
10875
10876        // Verify both segments are still in the sketch
10877        assert!(
10878            sketch_after.segments.contains(&arc_id),
10879            "Arc segment should still exist after failed constraint"
10880        );
10881        assert!(
10882            sketch_after.segments.contains(&line_id),
10883            "Line segment should still exist after failed constraint"
10884        );
10885
10886        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10887        let arc_obj = frontend
10888            .scene_graph
10889            .objects
10890            .get(arc_id.0)
10891            .expect("Arc object should still be accessible");
10892        let line_obj = frontend
10893            .scene_graph
10894            .objects
10895            .get(line_id.0)
10896            .expect("Line object should still be accessible");
10897
10898        // Verify source ranges are still valid (not corrupted)
10899        // Just verify that the objects are still accessible and have the expected types
10900        match &arc_obj.kind {
10901            ObjectKind::Segment {
10902                segment: Segment::Arc(_),
10903            } => {}
10904            _ => panic!("Arc object should still be an arc segment"),
10905        }
10906        match &line_obj.kind {
10907            ObjectKind::Segment {
10908                segment: Segment::Line(_),
10909            } => {}
10910            _ => panic!("Line object should still be a line segment"),
10911        }
10912
10913        ctx.close().await;
10914        mock_ctx.close().await;
10915    }
10916
10917    #[tokio::test(flavor = "multi_thread")]
10918    async fn test_distance_two_points() {
10919        let initial_source = "\
10920sketch(on = XY) {
10921  point(at = [var 1, var 2])
10922  point(at = [var 3, var 4])
10923}
10924";
10925
10926        let program = Program::parse(initial_source).unwrap().0.unwrap();
10927
10928        let mut frontend = FrontendState::new();
10929
10930        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10931        let mock_ctx = ExecutorContext::new_mock(None).await;
10932        let version = Version(0);
10933
10934        frontend.hack_set_program(&ctx, program).await.unwrap();
10935        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10936        let sketch_id = sketch_object.id;
10937        let sketch = expect_sketch(sketch_object);
10938        let point0_id = *sketch.segments.first().unwrap();
10939        let point1_id = *sketch.segments.get(1).unwrap();
10940
10941        let constraint = Constraint::Distance(Distance {
10942            segments: vec![point0_id.into(), point1_id.into()],
10943            distance: Number {
10944                value: 2.0,
10945                units: NumericSuffix::Mm,
10946            },
10947            label_position: None,
10948            source: Default::default(),
10949        });
10950        let (src_delta, scene_delta) = frontend
10951            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10952            .await
10953            .unwrap();
10954        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10955        assert_eq!(
10956            scene_delta.new_graph.objects.len(),
10957            5,
10958            "{:#?}",
10959            scene_delta.new_graph.objects
10960        );
10961
10962        ctx.close().await;
10963        mock_ctx.close().await;
10964    }
10965
10966    #[tokio::test(flavor = "multi_thread")]
10967    async fn test_distance_two_points_with_label() {
10968        let initial_source = "\
10969sketch(on = XY) {
10970  point(at = [var 1, var 2])
10971  point(at = [var 3, var 4])
10972}
10973";
10974
10975        let program = Program::parse(initial_source).unwrap().0.unwrap();
10976
10977        let mut frontend = FrontendState::new();
10978
10979        let mock_ctx = ExecutorContext::new_mock(None).await;
10980        let version = Version(0);
10981
10982        frontend.program = program.clone();
10983        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10984        frontend.update_state_after_exec(outcome, true);
10985        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10986        let sketch_id = sketch_object.id;
10987        let sketch = expect_sketch(sketch_object);
10988        let point0_id = *sketch.segments.first().unwrap();
10989        let point1_id = *sketch.segments.get(1).unwrap();
10990
10991        let label_position = Point2d {
10992            x: Number {
10993                value: 10.0,
10994                units: NumericSuffix::Mm,
10995            },
10996            y: Number {
10997                value: 11.0,
10998                units: NumericSuffix::Mm,
10999            },
11000        };
11001        let constraint = Constraint::Distance(Distance {
11002            segments: vec![point0_id.into(), point1_id.into()],
11003            distance: Number {
11004                value: 2.0,
11005                units: NumericSuffix::Mm,
11006            },
11007            label_position: Some(label_position.clone()),
11008            source: Default::default(),
11009        });
11010        let (src_delta, scene_delta) = frontend
11011            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11012            .await
11013            .unwrap();
11014        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11015
11016        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11017        let sketch = expect_sketch(sketch_object);
11018        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11019        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11020            panic!("Expected constraint object");
11021        };
11022        let Constraint::Distance(distance) = constraint else {
11023            panic!("Expected distance constraint");
11024        };
11025        assert_eq!(distance.label_position, Some(label_position));
11026
11027        mock_ctx.close().await;
11028    }
11029
11030    #[tokio::test(flavor = "multi_thread")]
11031    async fn test_edit_distance_constraint_label_position() {
11032        let initial_source = "\
11033sketch(on = XY) {
11034  point(at = [var 1, var 2])
11035  point(at = [var 3, var 2])
11036}
11037";
11038
11039        let program = Program::parse(initial_source).unwrap().0.unwrap();
11040
11041        let mut frontend = FrontendState::new();
11042
11043        let mock_ctx = ExecutorContext::new_mock(None).await;
11044        let version = Version(0);
11045
11046        frontend.program = program.clone();
11047        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11048        frontend.update_state_after_exec(outcome, true);
11049        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11050        let sketch_id = sketch_object.id;
11051        let sketch = expect_sketch(sketch_object);
11052        let point0_id = *sketch.segments.first().unwrap();
11053        let point1_id = *sketch.segments.get(1).unwrap();
11054
11055        let constraint = Constraint::Distance(Distance {
11056            segments: vec![point0_id.into(), point1_id.into()],
11057            distance: Number {
11058                value: 2.0,
11059                units: NumericSuffix::Mm,
11060            },
11061            label_position: None,
11062            source: Default::default(),
11063        });
11064        let (_, scene_delta) = frontend
11065            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11066            .await
11067            .unwrap();
11068        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069        let sketch = expect_sketch(sketch_object);
11070        let constraint_id = sketch.constraints[0];
11071        let label_position = Point2d {
11072            x: Number {
11073                value: 10.0,
11074                units: NumericSuffix::Mm,
11075            },
11076            y: Number {
11077                value: 11.0,
11078                units: NumericSuffix::Mm,
11079            },
11080        };
11081
11082        let (src_delta, scene_delta) = frontend
11083            .edit_distance_constraint_label_position(
11084                &mock_ctx,
11085                version,
11086                sketch_id,
11087                constraint_id,
11088                label_position.clone(),
11089                vec![],
11090            )
11091            .await
11092            .unwrap();
11093        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11094
11095        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11096        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11097            panic!("Expected constraint object");
11098        };
11099        let Constraint::Distance(distance) = constraint else {
11100            panic!("Expected distance constraint");
11101        };
11102        assert_eq!(distance.label_position, Some(label_position));
11103
11104        mock_ctx.close().await;
11105    }
11106
11107    #[tokio::test(flavor = "multi_thread")]
11108    async fn test_edit_distance_constraint_type_and_value() {
11109        let initial_source = "\
11110sketch(on = XY) {
11111  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11112  distance([line1.start, line1.end]) == 5mm
11113}
11114";
11115
11116        let program = Program::parse(initial_source).unwrap().0.unwrap();
11117        let mut frontend = FrontendState::new();
11118        let mock_ctx = ExecutorContext::new_mock(None).await;
11119        let version = Version(0);
11120
11121        frontend.program = program.clone();
11122        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11123        frontend.update_state_after_exec(outcome, true);
11124        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11125        let sketch_id = sketch_object.id;
11126        let sketch = expect_sketch(sketch_object);
11127        let constraint_id = sketch.constraints[0];
11128        let point0_id = sketch.segments[0];
11129        let point1_id = sketch.segments[1];
11130        let label_position = Point2d {
11131            x: Number {
11132                value: 2.0,
11133                units: NumericSuffix::Mm,
11134            },
11135            y: Number {
11136                value: 5.0,
11137                units: NumericSuffix::Mm,
11138            },
11139        };
11140
11141        let (source_delta, scene_delta) = frontend
11142            .edit_distance_constraint_with_options(
11143                &mock_ctx,
11144                version,
11145                sketch_id,
11146                constraint_id,
11147                Constraint::HorizontalDistance(Distance {
11148                    segments: vec![point0_id.into(), point1_id.into()],
11149                    distance: Number {
11150                        value: 4.0,
11151                        units: NumericSuffix::Mm,
11152                    },
11153                    label_position: Some(label_position.clone()),
11154                    source: Default::default(),
11155                }),
11156                EditConstraintOptions {
11157                    commit_solved_initial_guesses: false,
11158                },
11159            )
11160            .await
11161            .unwrap();
11162        assert_eq!(
11163            source_delta.text,
11164            "\
11165sketch(on = XY) {
11166  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11167  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11168}
11169"
11170        );
11171
11172        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11173        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11174            panic!("Expected constraint object");
11175        };
11176        let Constraint::HorizontalDistance(distance) = constraint else {
11177            panic!("Expected horizontal distance constraint");
11178        };
11179        assert_eq!(distance.distance.value, 4.0);
11180        assert_eq!(distance.label_position, Some(label_position));
11181
11182        mock_ctx.close().await;
11183    }
11184
11185    #[tokio::test(flavor = "multi_thread")]
11186    async fn test_edit_angle_constraint_label_position() {
11187        let initial_source = "\
11188sketch(on = XY) {
11189  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11190  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11191  angle([line1, line2]) == 60deg
11192}
11193";
11194
11195        let program = Program::parse(initial_source).unwrap().0.unwrap();
11196        let mut frontend = FrontendState::new();
11197        let mock_ctx = ExecutorContext::new_mock(None).await;
11198        let version = Version(0);
11199
11200        frontend.program = program.clone();
11201        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11202        frontend.update_state_after_exec(outcome, true);
11203        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11204        let sketch_id = sketch_object.id;
11205        let sketch = expect_sketch(sketch_object);
11206        let constraint_id = sketch.constraints[0];
11207        let label_position = Point2d {
11208            x: Number {
11209                value: 10.0,
11210                units: NumericSuffix::Mm,
11211            },
11212            y: Number {
11213                value: 11.0,
11214                units: NumericSuffix::Mm,
11215            },
11216        };
11217
11218        let (src_delta, scene_delta) = frontend
11219            .edit_distance_constraint_label_position(
11220                &mock_ctx,
11221                version,
11222                sketch_id,
11223                constraint_id,
11224                label_position.clone(),
11225                vec![],
11226            )
11227            .await
11228            .unwrap();
11229        assert_eq!(
11230            src_delta.text.as_str(),
11231            "\
11232sketch(on = XY) {
11233  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11234  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11235  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11236}
11237"
11238        );
11239
11240        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11241        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11242            panic!("Expected constraint object");
11243        };
11244        let Constraint::Angle(angle) = constraint else {
11245            panic!("Expected angle constraint");
11246        };
11247        assert_eq!(angle.label_position, Some(label_position));
11248
11249        mock_ctx.close().await;
11250    }
11251
11252    #[tokio::test(flavor = "multi_thread")]
11253    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11254        let initial_source = "\
11255sketch(on = XY) {
11256  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11257  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11258  60deg == angleDimension(lines = [line1, line2], sector = 1)
11259}
11260";
11261
11262        let program = Program::parse(initial_source).unwrap().0.unwrap();
11263        let mut frontend = FrontendState::new();
11264        let mock_ctx = ExecutorContext::new_mock(None).await;
11265        let version = Version(0);
11266
11267        frontend.program = program.clone();
11268        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11269        frontend.update_state_after_exec(outcome, true);
11270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11271        let sketch_id = sketch_object.id;
11272        let sketch = expect_sketch(sketch_object);
11273        let constraint_id = sketch.constraints[0];
11274        let label_position = Point2d {
11275            x: Number {
11276                value: 10.0,
11277                units: NumericSuffix::Mm,
11278            },
11279            y: Number {
11280                value: 11.0,
11281                units: NumericSuffix::Mm,
11282            },
11283        };
11284
11285        let (src_delta, scene_delta) = frontend
11286            .edit_distance_constraint_label_position(
11287                &mock_ctx,
11288                version,
11289                sketch_id,
11290                constraint_id,
11291                label_position.clone(),
11292                vec![],
11293            )
11294            .await
11295            .unwrap();
11296        assert_eq!(
11297            src_delta.text.as_str(),
11298            "\
11299sketch(on = XY) {
11300  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11301  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11302  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11303}
11304"
11305        );
11306
11307        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11308        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11309            panic!("Expected constraint object");
11310        };
11311        let Constraint::Angle(angle) = constraint else {
11312            panic!("Expected angle constraint");
11313        };
11314        assert_eq!(angle.label_position, Some(label_position));
11315
11316        mock_ctx.close().await;
11317    }
11318
11319    #[tokio::test(flavor = "multi_thread")]
11320    async fn test_edit_angle_constraint() {
11321        let initial_source = "\
11322sketch(on = XY) {
11323  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11324  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11325  angle([line1, line2]) == 60deg
11326}
11327";
11328
11329        let program = Program::parse(initial_source).unwrap().0.unwrap();
11330        let mut frontend = FrontendState::new();
11331        let mock_ctx = ExecutorContext::new_mock(None).await;
11332        let version = Version(0);
11333
11334        frontend.program = program.clone();
11335        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11336        frontend.update_state_after_exec(outcome, true);
11337        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11338        let sketch_id = sketch_object.id;
11339        let sketch = expect_sketch(sketch_object);
11340        let constraint_id = sketch.constraints[0];
11341        let line1_id = *sketch.segments.get(2).unwrap();
11342        let line2_id = *sketch.segments.get(5).unwrap();
11343        let label_position = Point2d {
11344            x: Number {
11345                value: 10.0,
11346                units: NumericSuffix::Mm,
11347            },
11348            y: Number {
11349                value: 11.0,
11350                units: NumericSuffix::Mm,
11351            },
11352        };
11353
11354        let (src_delta, scene_delta) = frontend
11355            .edit_angle_constraint_with_options(
11356                &mock_ctx,
11357                version,
11358                sketch_id,
11359                constraint_id,
11360                Angle {
11361                    lines: vec![line2_id, line1_id],
11362                    angle: Number {
11363                        value: 60.0,
11364                        units: NumericSuffix::Deg,
11365                    },
11366                    sector: Some(3),
11367                    inverse: Some(false),
11368                    label_position: Some(label_position.clone()),
11369                    source: Default::default(),
11370                },
11371                EditConstraintOptions {
11372                    commit_solved_initial_guesses: false,
11373                },
11374            )
11375            .await
11376            .unwrap();
11377        assert_eq!(
11378            src_delta.text.as_str(),
11379            "\
11380sketch(on = XY) {
11381  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11382  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11383  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11384}
11385"
11386        );
11387
11388        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11389        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11390            panic!("Expected constraint object");
11391        };
11392        let Constraint::Angle(angle) = constraint else {
11393            panic!("Expected angle constraint");
11394        };
11395        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11396        assert_eq!(angle.sector, Some(3));
11397        assert_eq!(angle.inverse, Some(false));
11398        assert_eq!(angle.label_position, Some(label_position));
11399
11400        mock_ctx.close().await;
11401    }
11402
11403    #[tokio::test(flavor = "multi_thread")]
11404    async fn test_edit_angle_constraint_with_call_on_right() {
11405        let initial_source = "\
11406sketch(on = XY) {
11407  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11408  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11409  60deg == angle([line1, line2])
11410}
11411";
11412
11413        let program = Program::parse(initial_source).unwrap().0.unwrap();
11414        let mut frontend = FrontendState::new();
11415        let mock_ctx = ExecutorContext::new_mock(None).await;
11416        let version = Version(0);
11417
11418        frontend.program = program.clone();
11419        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11420        frontend.update_state_after_exec(outcome, true);
11421        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11422        let sketch_id = sketch_object.id;
11423        let sketch = expect_sketch(sketch_object);
11424        let constraint_id = sketch.constraints[0];
11425        let line1_id = *sketch.segments.get(2).unwrap();
11426        let line2_id = *sketch.segments.get(5).unwrap();
11427
11428        let (src_delta, _) = frontend
11429            .edit_angle_constraint_with_options(
11430                &mock_ctx,
11431                version,
11432                sketch_id,
11433                constraint_id,
11434                Angle {
11435                    lines: vec![line2_id, line1_id],
11436                    angle: Number {
11437                        value: 60.0,
11438                        units: NumericSuffix::Deg,
11439                    },
11440                    sector: Some(3),
11441                    inverse: Some(false),
11442                    label_position: None,
11443                    source: Default::default(),
11444                },
11445                EditConstraintOptions {
11446                    commit_solved_initial_guesses: false,
11447                },
11448            )
11449            .await
11450            .unwrap();
11451        assert_eq!(
11452            src_delta.text.as_str(),
11453            "\
11454sketch(on = XY) {
11455  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11456  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11457  60deg == angleDimension(lines = [line2, line1], sector = 3)
11458}
11459"
11460        );
11461
11462        mock_ctx.close().await;
11463    }
11464
11465    #[tokio::test(flavor = "multi_thread")]
11466    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11467        let initial_source = "\
11468@settings(kclVersion = 2.0)
11469
11470sketch001 = sketch(on = XZ) {
11471  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11472  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11473  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11474  vertical([line1.start, ORIGIN])
11475}
11476";
11477
11478        let program = Program::parse(initial_source).unwrap().0.unwrap();
11479        let mut frontend = FrontendState::new();
11480        let mock_ctx = ExecutorContext::new_mock(None).await;
11481        let version = Version(0);
11482
11483        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11484        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11485        let sketch_id = sketch_object.id;
11486        let sketch = expect_sketch(sketch_object);
11487        let constraint_id = sketch
11488            .constraints
11489            .iter()
11490            .copied()
11491            .find(|constraint_id| {
11492                matches!(
11493                    frontend.scene_graph.objects[constraint_id.0].kind,
11494                    ObjectKind::Constraint {
11495                        constraint: Constraint::Distance(_)
11496                    }
11497                )
11498            })
11499            .unwrap();
11500        let line1_id = sketch
11501            .segments
11502            .iter()
11503            .copied()
11504            .find(|segment_id| {
11505                matches!(
11506                    frontend.scene_graph.objects[segment_id.0].kind,
11507                    ObjectKind::Segment {
11508                        segment: Segment::Line(_)
11509                    }
11510                )
11511            })
11512            .unwrap();
11513        let label_position = Point2d {
11514            x: Number {
11515                value: 7.0,
11516                units: NumericSuffix::Mm,
11517            },
11518            y: Number {
11519                value: 9.0,
11520                units: NumericSuffix::Mm,
11521            },
11522        };
11523
11524        let (source_delta, scene_delta) = frontend
11525            .edit_segments_with_options(
11526                &mock_ctx,
11527                version,
11528                sketch_id,
11529                vec![ExistingSegmentCtor {
11530                    id: line1_id,
11531                    ctor: SegmentCtor::Line(LineCtor {
11532                        start: point_expr_mm(2.0, 15.55),
11533                        end: point_expr_mm(-4.03, 11.51),
11534                        construction: None,
11535                    }),
11536                }],
11537                EditSegmentsOptions {
11538                    anchor_segment_ids: Some(vec![]),
11539                    drag_anchors: vec![SegmentDragAnchor {
11540                        segment_id: line1_id,
11541                        target: label_position.clone(),
11542                    }],
11543                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11544                        constraint_id,
11545                        label_position: label_position.clone(),
11546                    }],
11547                    commit_solved_initial_guesses: true,
11548                },
11549            )
11550            .await
11551            .unwrap();
11552
11553        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11554        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11555        let ObjectKind::Constraint {
11556            constraint: Constraint::Distance(distance),
11557        } = &constraint_object.kind
11558        else {
11559            panic!("Expected distance constraint object");
11560        };
11561        assert_eq!(distance.label_position, Some(label_position));
11562
11563        let snapped_label_position = Point2d {
11564            x: Number {
11565                value: 8.0,
11566                units: NumericSuffix::Mm,
11567            },
11568            y: Number {
11569                value: 10.0,
11570                units: NumericSuffix::Mm,
11571            },
11572        };
11573        let (source_delta, scene_delta) = frontend
11574            .edit_segments_with_options(
11575                &mock_ctx,
11576                version,
11577                sketch_id,
11578                vec![],
11579                EditSegmentsOptions {
11580                    anchor_segment_ids: Some(vec![line1_id]),
11581                    drag_anchors: vec![],
11582                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11583                        constraint_id,
11584                        label_position: snapped_label_position.clone(),
11585                    }],
11586                    commit_solved_initial_guesses: true,
11587                },
11588            )
11589            .await
11590            .unwrap();
11591
11592        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11593        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11594        let ObjectKind::Constraint {
11595            constraint: Constraint::Distance(distance),
11596        } = &constraint_object.kind
11597        else {
11598            panic!("Expected distance constraint object");
11599        };
11600        assert_eq!(distance.label_position, Some(snapped_label_position));
11601
11602        mock_ctx.close().await;
11603    }
11604
11605    #[tokio::test(flavor = "multi_thread")]
11606    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11607        let initial_source = "\
11608sketch(on = XY) {
11609  point1 = point(at = [var 0mm, var 0mm])
11610  point2 = point(at = [var 10mm, var 0mm])
11611  distance([point1, point2]) == 5mm
11612}
11613";
11614
11615        let program = Program::parse(initial_source).unwrap().0.unwrap();
11616        let mut frontend = FrontendState::new();
11617        let mock_ctx = ExecutorContext::new_mock(None).await;
11618        let version = Version(0);
11619
11620        frontend.program = program.clone();
11621        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11622        frontend.update_state_after_exec(outcome, true);
11623        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11624        let sketch_id = sketch_object.id;
11625        let sketch = expect_sketch(sketch_object);
11626        let point0_id = sketch.segments[0];
11627        let point1_id = sketch.segments[1];
11628        let constraint_id = sketch.constraints[0];
11629
11630        let edited_segments = vec![ExistingSegmentCtor {
11631            id: point0_id,
11632            ctor: SegmentCtor::Point(PointCtor {
11633                position: Point2d {
11634                    x: Expr::Var(Number {
11635                        value: 2.0,
11636                        units: NumericSuffix::Mm,
11637                    }),
11638                    y: Expr::Var(Number {
11639                        value: 1.0,
11640                        units: NumericSuffix::Mm,
11641                    }),
11642                },
11643            }),
11644        }];
11645        let (_, scene_delta) = frontend
11646            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11647            .await
11648            .unwrap();
11649        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11650        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11651
11652        let label_position = Point2d {
11653            x: Number {
11654                value: 3.0,
11655                units: NumericSuffix::Mm,
11656            },
11657            y: Number {
11658                value: 4.0,
11659                units: NumericSuffix::Mm,
11660            },
11661        };
11662        let (_, scene_delta) = frontend
11663            .edit_distance_constraint_label_position(
11664                &mock_ctx,
11665                version,
11666                sketch_id,
11667                constraint_id,
11668                label_position,
11669                vec![point0_id],
11670            )
11671            .await
11672            .unwrap();
11673
11674        assert_point_position_close(
11675            point_position(&scene_delta.new_graph, point0_id),
11676            point0_after_segment_edit,
11677        );
11678        assert_point_position_close(
11679            point_position(&scene_delta.new_graph, point1_id),
11680            point1_after_segment_edit,
11681        );
11682
11683        mock_ctx.close().await;
11684    }
11685
11686    #[tokio::test(flavor = "multi_thread")]
11687    async fn test_distance_point_line() {
11688        let initial_source = "\
11689sketch(on = XY) {
11690  point(at = [var 0, var 5])
11691  line(start = [var 0, var 0], end = [var 10, var 0])
11692}
11693";
11694
11695        let program = Program::parse(initial_source).unwrap().0.unwrap();
11696
11697        let mut frontend = FrontendState::new();
11698
11699        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11700        let mock_ctx = ExecutorContext::new_mock(None).await;
11701        let version = Version(0);
11702
11703        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11704        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11705        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11706        let sketch_id = sketch_object.id;
11707        let sketch = expect_sketch(sketch_object);
11708        let point_id = *sketch.segments.first().unwrap();
11709        let line_id = *sketch
11710            .segments
11711            .iter()
11712            .find(|segment_id| {
11713                matches!(
11714                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11715                    Some(ObjectKind::Segment {
11716                        segment: Segment::Line(_)
11717                    })
11718                )
11719            })
11720            .unwrap();
11721
11722        let label_position = Point2d {
11723            x: Number {
11724                value: 10.0,
11725                units: NumericSuffix::Mm,
11726            },
11727            y: Number {
11728                value: 11.0,
11729                units: NumericSuffix::Mm,
11730            },
11731        };
11732        let constraint = Constraint::Distance(Distance {
11733            segments: vec![point_id.into(), line_id.into()],
11734            distance: Number {
11735                value: 5.0,
11736                units: NumericSuffix::Mm,
11737            },
11738            label_position: Some(label_position.clone()),
11739            source: Default::default(),
11740        });
11741        let (src_delta, scene_delta) = frontend
11742            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11743            .await
11744            .unwrap();
11745        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11746        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11747        let sketch = expect_sketch(sketch_object);
11748        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11749        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11750            panic!("Expected constraint object");
11751        };
11752        let Constraint::Distance(distance) = constraint else {
11753            panic!("Expected distance constraint");
11754        };
11755        assert_eq!(distance.label_position, Some(label_position));
11756
11757        ctx.close().await;
11758        mock_ctx.close().await;
11759    }
11760
11761    #[tokio::test(flavor = "multi_thread")]
11762    async fn test_distance_point_arc() {
11763        let initial_source = "\
11764sketch(on = XY) {
11765  point(at = [var 0, var 8])
11766  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11767}
11768";
11769
11770        let program = Program::parse(initial_source).unwrap().0.unwrap();
11771
11772        let mut frontend = FrontendState::new();
11773
11774        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11775        let mock_ctx = ExecutorContext::new_mock(None).await;
11776        let version = Version(0);
11777
11778        frontend.hack_set_program(&ctx, program).await.unwrap();
11779        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11780        let sketch_id = sketch_object.id;
11781        let sketch = expect_sketch(sketch_object);
11782        let point_id = *sketch.segments.first().unwrap();
11783        let arc_id = *sketch
11784            .segments
11785            .iter()
11786            .find(|segment_id| {
11787                matches!(
11788                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11789                    Some(ObjectKind::Segment {
11790                        segment: Segment::Arc(_)
11791                    })
11792                )
11793            })
11794            .unwrap();
11795
11796        let constraint = Constraint::Distance(Distance {
11797            segments: vec![point_id.into(), arc_id.into()],
11798            distance: Number {
11799                value: 3.0,
11800                units: NumericSuffix::Mm,
11801            },
11802            label_position: None,
11803            source: Default::default(),
11804        });
11805        let (src_delta, _scene_delta) = frontend
11806            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11807            .await
11808            .unwrap();
11809        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11810
11811        ctx.close().await;
11812        mock_ctx.close().await;
11813    }
11814
11815    #[tokio::test(flavor = "multi_thread")]
11816    async fn test_distance_arc_origin() {
11817        let initial_source = "\
11818sketch001 = sketch(on = XY) {
11819  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11820}
11821";
11822
11823        let program = Program::parse(initial_source).unwrap().0.unwrap();
11824
11825        let mut frontend = FrontendState::new();
11826
11827        let mock_ctx = ExecutorContext::new_mock(None).await;
11828        let version = Version(0);
11829
11830        frontend.program = program.clone();
11831        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11832        frontend.update_state_after_exec(outcome, true);
11833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11834        let sketch_id = sketch_object.id;
11835        let sketch = expect_sketch(sketch_object);
11836        let arc_id = *sketch
11837            .segments
11838            .iter()
11839            .find(|segment_id| {
11840                matches!(
11841                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11842                    Some(ObjectKind::Segment {
11843                        segment: Segment::Arc(_)
11844                    })
11845                )
11846            })
11847            .unwrap();
11848
11849        let constraint = Constraint::Distance(Distance {
11850            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11851            distance: Number {
11852                value: 3.0,
11853                units: NumericSuffix::Mm,
11854            },
11855            label_position: None,
11856            source: Default::default(),
11857        });
11858        let (src_delta, _scene_delta) = frontend
11859            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11860            .await
11861            .unwrap();
11862        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11863
11864        mock_ctx.close().await;
11865    }
11866
11867    #[tokio::test(flavor = "multi_thread")]
11868    async fn test_distance_line_origin() {
11869        let initial_source = "\
11870sketch(on = XY) {
11871  line(start = [var 5, var 0], end = [var 5, var 10])
11872}
11873";
11874
11875        let program = Program::parse(initial_source).unwrap().0.unwrap();
11876
11877        let mut frontend = FrontendState::new();
11878
11879        let mock_ctx = ExecutorContext::new_mock(None).await;
11880        let version = Version(0);
11881
11882        frontend.program = program.clone();
11883        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11884        frontend.update_state_after_exec(outcome, true);
11885        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11886        let sketch_id = sketch_object.id;
11887        let sketch = expect_sketch(sketch_object);
11888        let line_id = *sketch
11889            .segments
11890            .iter()
11891            .find(|segment_id| {
11892                matches!(
11893                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11894                    Some(ObjectKind::Segment {
11895                        segment: Segment::Line(_)
11896                    })
11897                )
11898            })
11899            .unwrap();
11900
11901        let constraint = Constraint::Distance(Distance {
11902            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11903            distance: Number {
11904                value: 5.0,
11905                units: NumericSuffix::Mm,
11906            },
11907            label_position: None,
11908            source: Default::default(),
11909        });
11910        let (src_delta, _scene_delta) = frontend
11911            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11912            .await
11913            .unwrap();
11914        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11915
11916        mock_ctx.close().await;
11917    }
11918
11919    #[tokio::test(flavor = "multi_thread")]
11920    async fn test_distance_line_circle() {
11921        let initial_source = "\
11922sketch(on = XY) {
11923  line(start = [var -10, var 8], end = [var 10, var 8])
11924  circle(start = [var 5, var 0], center = [var 0, var 0])
11925}
11926";
11927
11928        let program = Program::parse(initial_source).unwrap().0.unwrap();
11929
11930        let mut frontend = FrontendState::new();
11931
11932        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11933        let mock_ctx = ExecutorContext::new_mock(None).await;
11934        let version = Version(0);
11935
11936        frontend.hack_set_program(&ctx, program).await.unwrap();
11937        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11938        let sketch_id = sketch_object.id;
11939        let sketch = expect_sketch(sketch_object);
11940        let line_id = *sketch
11941            .segments
11942            .iter()
11943            .find(|segment_id| {
11944                matches!(
11945                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11946                    Some(ObjectKind::Segment {
11947                        segment: Segment::Line(_)
11948                    })
11949                )
11950            })
11951            .unwrap();
11952        let circle_id = *sketch
11953            .segments
11954            .iter()
11955            .find(|segment_id| {
11956                matches!(
11957                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11958                    Some(ObjectKind::Segment {
11959                        segment: Segment::Circle(_)
11960                    })
11961                )
11962            })
11963            .unwrap();
11964
11965        let constraint = Constraint::Distance(Distance {
11966            segments: vec![line_id.into(), circle_id.into()],
11967            distance: Number {
11968                value: 3.0,
11969                units: NumericSuffix::Mm,
11970            },
11971            label_position: None,
11972            source: Default::default(),
11973        });
11974        let (src_delta, _scene_delta) = frontend
11975            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11976            .await
11977            .unwrap();
11978        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11979
11980        ctx.close().await;
11981        mock_ctx.close().await;
11982    }
11983
11984    #[tokio::test(flavor = "multi_thread")]
11985    async fn test_distance_circle_arc() {
11986        let initial_source = "\
11987sketch(on = XY) {
11988  circle(start = [var 5, var 0], center = [var 0, var 0])
11989  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11990}
11991";
11992
11993        let program = Program::parse(initial_source).unwrap().0.unwrap();
11994
11995        let mut frontend = FrontendState::new();
11996
11997        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11998        let mock_ctx = ExecutorContext::new_mock(None).await;
11999        let version = Version(0);
12000
12001        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12002        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12003        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12004        let sketch_id = sketch_object.id;
12005        let sketch = expect_sketch(sketch_object);
12006        let circle_id = *sketch
12007            .segments
12008            .iter()
12009            .find(|segment_id| {
12010                matches!(
12011                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12012                    Some(ObjectKind::Segment {
12013                        segment: Segment::Circle(_)
12014                    })
12015                )
12016            })
12017            .unwrap();
12018        let arc_id = *sketch
12019            .segments
12020            .iter()
12021            .find(|segment_id| {
12022                matches!(
12023                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12024                    Some(ObjectKind::Segment {
12025                        segment: Segment::Arc(_)
12026                    })
12027                )
12028            })
12029            .unwrap();
12030
12031        let constraint = Constraint::Distance(Distance {
12032            segments: vec![circle_id.into(), arc_id.into()],
12033            distance: Number {
12034                value: 3.0,
12035                units: NumericSuffix::Mm,
12036            },
12037            label_position: None,
12038            source: Default::default(),
12039        });
12040        let (src_delta, _scene_delta) = frontend
12041            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12042            .await
12043            .unwrap();
12044        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12045
12046        ctx.close().await;
12047        mock_ctx.close().await;
12048    }
12049
12050    #[tokio::test(flavor = "multi_thread")]
12051    async fn test_distance_parallel_lines() {
12052        let initial_source = "\
12053sketch(on = XY) {
12054  line(start = [var 0, var 0], end = [var 10, var 0])
12055  line(start = [var 0, var 5], end = [var 10, var 5])
12056}
12057";
12058
12059        let program = Program::parse(initial_source).unwrap().0.unwrap();
12060
12061        let mut frontend = FrontendState::new();
12062
12063        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12064        let mock_ctx = ExecutorContext::new_mock(None).await;
12065        let version = Version(0);
12066
12067        frontend.hack_set_program(&ctx, program).await.unwrap();
12068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12069        let sketch_id = sketch_object.id;
12070        let sketch = expect_sketch(sketch_object);
12071        let line_ids = sketch
12072            .segments
12073            .iter()
12074            .copied()
12075            .filter(|segment_id| {
12076                matches!(
12077                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12078                    Some(ObjectKind::Segment {
12079                        segment: Segment::Line(_)
12080                    })
12081                )
12082            })
12083            .collect::<Vec<_>>();
12084
12085        let constraint = Constraint::Distance(Distance {
12086            segments: vec![line_ids[0].into(), line_ids[1].into()],
12087            distance: Number {
12088                value: 5.0,
12089                units: NumericSuffix::Mm,
12090            },
12091            label_position: None,
12092            source: Default::default(),
12093        });
12094        let (src_delta, _scene_delta) = frontend
12095            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12096            .await
12097            .unwrap();
12098        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12099
12100        ctx.close().await;
12101        mock_ctx.close().await;
12102    }
12103
12104    #[tokio::test(flavor = "multi_thread")]
12105    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12106        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12107        // one another because the gradient of the residual has no tangential component in this
12108        // configuration. The only path to reducing the residual is shrinking the lengths of the
12109        // lines which are causing the lines to collapse, producing a degenerate output.
12110        let initial_source = "\
12111sketch(on = XY) {
12112  line(start = [var 0, var 0], end = [var 10, var 0])
12113  line(start = [var 0, var 0], end = [var 10, var 10])
12114}
12115";
12116
12117        let program = Program::parse(initial_source).unwrap().0.unwrap();
12118
12119        let mut frontend = FrontendState::new();
12120
12121        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12122        let mock_ctx = ExecutorContext::new_mock(None).await;
12123        let version = Version(0);
12124
12125        frontend.hack_set_program(&ctx, program).await.unwrap();
12126        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12127        let sketch_id = sketch_object.id;
12128        let sketch = expect_sketch(sketch_object);
12129        let line_ids = sketch
12130            .segments
12131            .iter()
12132            .copied()
12133            .filter(|segment_id| {
12134                matches!(
12135                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12136                    Some(ObjectKind::Segment {
12137                        segment: Segment::Line(_)
12138                    })
12139                )
12140            })
12141            .collect::<Vec<_>>();
12142
12143        let constraint = Constraint::Distance(Distance {
12144            segments: vec![line_ids[0].into(), line_ids[1].into()],
12145            distance: Number {
12146                value: 5.0,
12147                units: NumericSuffix::Mm,
12148            },
12149            label_position: None,
12150            source: Default::default(),
12151        });
12152        let (src_delta, _scene_delta) = frontend
12153            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12154            .await
12155            .unwrap();
12156        insta::assert_snapshot!(
12157            "test_distance_non_parallel_lines_lowers_to_distance",
12158            src_delta.text.as_str()
12159        );
12160
12161        ctx.close().await;
12162        mock_ctx.close().await;
12163    }
12164
12165    #[tokio::test(flavor = "multi_thread")]
12166    async fn test_horizontal_distance_two_points() {
12167        let initial_source = "\
12168sketch(on = XY) {
12169  point(at = [var 1, var 2])
12170  point(at = [var 3, var 4])
12171}
12172";
12173
12174        let program = Program::parse(initial_source).unwrap().0.unwrap();
12175
12176        let mut frontend = FrontendState::new();
12177
12178        let mock_ctx = ExecutorContext::new_mock(None).await;
12179        let version = Version(0);
12180
12181        frontend.program = program.clone();
12182        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12183        frontend.update_state_after_exec(outcome, true);
12184        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12185        let sketch_id = sketch_object.id;
12186        let sketch = expect_sketch(sketch_object);
12187        let point0_id = *sketch.segments.first().unwrap();
12188        let point1_id = *sketch.segments.get(1).unwrap();
12189        let label_position = Point2d {
12190            x: Number {
12191                value: 10.0,
12192                units: NumericSuffix::Mm,
12193            },
12194            y: Number {
12195                value: 11.0,
12196                units: NumericSuffix::Mm,
12197            },
12198        };
12199
12200        let constraint = Constraint::HorizontalDistance(Distance {
12201            segments: vec![point0_id.into(), point1_id.into()],
12202            distance: Number {
12203                value: 2.0,
12204                units: NumericSuffix::Mm,
12205            },
12206            label_position: Some(label_position.clone()),
12207            source: Default::default(),
12208        });
12209        let (src_delta, scene_delta) = frontend
12210            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12211            .await
12212            .unwrap();
12213        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12214        assert_eq!(
12215            scene_delta.new_graph.objects.len(),
12216            5,
12217            "{:#?}",
12218            scene_delta.new_graph.objects
12219        );
12220        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12221        let sketch = expect_sketch(sketch_object);
12222        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12223        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12224            panic!("Expected constraint object");
12225        };
12226        let Constraint::HorizontalDistance(distance) = constraint else {
12227            panic!("Expected horizontal distance constraint");
12228        };
12229        assert_eq!(distance.label_position, Some(label_position));
12230
12231        mock_ctx.close().await;
12232    }
12233
12234    #[tokio::test(flavor = "multi_thread")]
12235    async fn test_radius_single_arc_segment() {
12236        let initial_source = "\
12237sketch(on = XY) {
12238  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12239}
12240";
12241
12242        let program = Program::parse(initial_source).unwrap().0.unwrap();
12243
12244        let mut frontend = FrontendState::new();
12245
12246        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12247        let mock_ctx = ExecutorContext::new_mock(None).await;
12248        let version = Version(0);
12249
12250        frontend.hack_set_program(&ctx, program).await.unwrap();
12251        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12252        let sketch_id = sketch_object.id;
12253        let sketch = expect_sketch(sketch_object);
12254        // Find the arc segment (not the points)
12255        let arc_id = sketch
12256            .segments
12257            .iter()
12258            .find(|&seg_id| {
12259                let obj = frontend.scene_graph.objects.get(seg_id.0);
12260                matches!(
12261                    obj.map(|o| &o.kind),
12262                    Some(ObjectKind::Segment {
12263                        segment: Segment::Arc(_)
12264                    })
12265                )
12266            })
12267            .unwrap();
12268
12269        let constraint = Constraint::Radius(Radius {
12270            arc: *arc_id,
12271            radius: Number {
12272                value: 5.0,
12273                units: NumericSuffix::Mm,
12274            },
12275            label_position: None,
12276            source: Default::default(),
12277        });
12278        let (src_delta, scene_delta) = frontend
12279            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12280            .await
12281            .unwrap();
12282        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12283        assert_eq!(
12284            scene_delta.new_graph.objects.len(),
12285            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12286            "{:#?}",
12287            scene_delta.new_graph.objects
12288        );
12289
12290        ctx.close().await;
12291        mock_ctx.close().await;
12292    }
12293
12294    #[tokio::test(flavor = "multi_thread")]
12295    async fn test_radius_single_arc_segment_with_label_position() {
12296        let initial_source = "\
12297sketch(on = XY) {
12298  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12299}
12300";
12301
12302        let program = Program::parse(initial_source).unwrap().0.unwrap();
12303        let mut frontend = FrontendState::new();
12304        let mock_ctx = ExecutorContext::new_mock(None).await;
12305        let version = Version(0);
12306
12307        frontend.program = program.clone();
12308        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12309        frontend.update_state_after_exec(outcome, true);
12310        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12311        let sketch_id = sketch_object.id;
12312        let sketch = expect_sketch(sketch_object);
12313        let arc_id = sketch
12314            .segments
12315            .iter()
12316            .find(|&seg_id| {
12317                let obj = frontend.scene_graph.objects.get(seg_id.0);
12318                matches!(
12319                    obj.map(|o| &o.kind),
12320                    Some(ObjectKind::Segment {
12321                        segment: Segment::Arc(_)
12322                    })
12323                )
12324            })
12325            .unwrap();
12326
12327        let label_position = Point2d {
12328            x: Number {
12329                value: 10.0,
12330                units: NumericSuffix::Mm,
12331            },
12332            y: Number {
12333                value: 11.0,
12334                units: NumericSuffix::Mm,
12335            },
12336        };
12337        let constraint = Constraint::Radius(Radius {
12338            arc: *arc_id,
12339            radius: Number {
12340                value: 5.0,
12341                units: NumericSuffix::Mm,
12342            },
12343            label_position: Some(label_position.clone()),
12344            source: Default::default(),
12345        });
12346        let (src_delta, scene_delta) = frontend
12347            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12348            .await
12349            .unwrap();
12350        insta::assert_snapshot!(
12351            "test_radius_single_arc_segment_with_label_position",
12352            src_delta.text.as_str()
12353        );
12354
12355        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12356        let sketch = expect_sketch(sketch_object);
12357        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12358        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12359            panic!("Expected constraint object");
12360        };
12361        let Constraint::Radius(radius) = constraint else {
12362            panic!("Expected radius constraint");
12363        };
12364        assert_eq!(radius.label_position, Some(label_position));
12365
12366        mock_ctx.close().await;
12367    }
12368
12369    #[tokio::test(flavor = "multi_thread")]
12370    async fn test_edit_radius_constraint_label_position() {
12371        let initial_source = "\
12372sketch(on = XY) {
12373  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12374  radius(arc1) == 5mm
12375}
12376";
12377
12378        let program = Program::parse(initial_source).unwrap().0.unwrap();
12379        let mut frontend = FrontendState::new();
12380        let mock_ctx = ExecutorContext::new_mock(None).await;
12381        let version = Version(0);
12382
12383        frontend.program = program.clone();
12384        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12385        frontend.update_state_after_exec(outcome, true);
12386        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12387        let sketch_id = sketch_object.id;
12388        let sketch = expect_sketch(sketch_object);
12389        let constraint_id = sketch.constraints[0];
12390        let label_position = Point2d {
12391            x: Number {
12392                value: 10.0,
12393                units: NumericSuffix::Mm,
12394            },
12395            y: Number {
12396                value: 11.0,
12397                units: NumericSuffix::Mm,
12398            },
12399        };
12400
12401        let (src_delta, scene_delta) = frontend
12402            .edit_distance_constraint_label_position(
12403                &mock_ctx,
12404                version,
12405                sketch_id,
12406                constraint_id,
12407                label_position.clone(),
12408                vec![],
12409            )
12410            .await
12411            .unwrap();
12412        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12413
12414        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12415        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12416            panic!("Expected constraint object");
12417        };
12418        let Constraint::Radius(radius) = constraint else {
12419            panic!("Expected radius constraint");
12420        };
12421        assert_eq!(radius.label_position, Some(label_position));
12422
12423        mock_ctx.close().await;
12424    }
12425
12426    #[tokio::test(flavor = "multi_thread")]
12427    async fn test_vertical_distance_two_points() {
12428        let initial_source = "\
12429sketch(on = XY) {
12430  point(at = [var 1, var 2])
12431  point(at = [var 3, var 4])
12432}
12433";
12434
12435        let program = Program::parse(initial_source).unwrap().0.unwrap();
12436
12437        let mut frontend = FrontendState::new();
12438
12439        let mock_ctx = ExecutorContext::new_mock(None).await;
12440        let version = Version(0);
12441
12442        frontend.program = program.clone();
12443        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12444        frontend.update_state_after_exec(outcome, true);
12445        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12446        let sketch_id = sketch_object.id;
12447        let sketch = expect_sketch(sketch_object);
12448        let point0_id = *sketch.segments.first().unwrap();
12449        let point1_id = *sketch.segments.get(1).unwrap();
12450        let label_position = Point2d {
12451            x: Number {
12452                value: 10.0,
12453                units: NumericSuffix::Mm,
12454            },
12455            y: Number {
12456                value: 11.0,
12457                units: NumericSuffix::Mm,
12458            },
12459        };
12460
12461        let constraint = Constraint::VerticalDistance(Distance {
12462            segments: vec![point0_id.into(), point1_id.into()],
12463            distance: Number {
12464                value: 2.0,
12465                units: NumericSuffix::Mm,
12466            },
12467            label_position: Some(label_position.clone()),
12468            source: Default::default(),
12469        });
12470        let (src_delta, scene_delta) = frontend
12471            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12472            .await
12473            .unwrap();
12474        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12475        assert_eq!(
12476            scene_delta.new_graph.objects.len(),
12477            5,
12478            "{:#?}",
12479            scene_delta.new_graph.objects
12480        );
12481        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12482        let sketch = expect_sketch(sketch_object);
12483        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12484        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12485            panic!("Expected constraint object");
12486        };
12487        let Constraint::VerticalDistance(distance) = constraint else {
12488            panic!("Expected vertical distance constraint");
12489        };
12490        assert_eq!(distance.label_position, Some(label_position));
12491
12492        mock_ctx.close().await;
12493    }
12494
12495    #[tokio::test(flavor = "multi_thread")]
12496    async fn test_add_fixed_standalone_point() {
12497        let initial_source = "\
12498sketch(on = XY) {
12499  point(at = [var 1, var 2])
12500}
12501";
12502
12503        let program = Program::parse(initial_source).unwrap().0.unwrap();
12504
12505        let mut frontend = FrontendState::new();
12506
12507        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12508        let mock_ctx = ExecutorContext::new_mock(None).await;
12509        let version = Version(0);
12510
12511        frontend.hack_set_program(&ctx, program).await.unwrap();
12512        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12513        let sketch_id = sketch_object.id;
12514        let sketch = expect_sketch(sketch_object);
12515        let point_id = *sketch.segments.first().unwrap();
12516
12517        let (src_delta, scene_delta) = frontend
12518            .add_constraint(
12519                &mock_ctx,
12520                version,
12521                sketch_id,
12522                Constraint::Fixed(Fixed {
12523                    points: vec![FixedPoint {
12524                        point: point_id,
12525                        position: Point2d {
12526                            x: Number {
12527                                value: 2.0,
12528                                units: NumericSuffix::Mm,
12529                            },
12530                            y: Number {
12531                                value: 3.0,
12532                                units: NumericSuffix::Mm,
12533                            },
12534                        },
12535                    }],
12536                }),
12537            )
12538            .await
12539            .unwrap();
12540        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12541        assert_eq!(
12542            scene_delta.new_graph.objects.len(),
12543            4,
12544            "{:#?}",
12545            scene_delta.new_graph.objects
12546        );
12547
12548        ctx.close().await;
12549        mock_ctx.close().await;
12550    }
12551
12552    #[tokio::test(flavor = "multi_thread")]
12553    async fn test_add_fixed_multiple_points() {
12554        let initial_source = "\
12555sketch(on = XY) {
12556  point(at = [var 1, var 2])
12557  point(at = [var 3, var 4])
12558}
12559";
12560
12561        let program = Program::parse(initial_source).unwrap().0.unwrap();
12562
12563        let mut frontend = FrontendState::new();
12564
12565        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12566        let mock_ctx = ExecutorContext::new_mock(None).await;
12567        let version = Version(0);
12568
12569        frontend.hack_set_program(&ctx, program).await.unwrap();
12570        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12571        let sketch_id = sketch_object.id;
12572        let sketch = expect_sketch(sketch_object);
12573        let point0_id = *sketch.segments.first().unwrap();
12574        let point1_id = *sketch.segments.get(1).unwrap();
12575
12576        let (src_delta, scene_delta) = frontend
12577            .add_constraint(
12578                &mock_ctx,
12579                version,
12580                sketch_id,
12581                Constraint::Fixed(Fixed {
12582                    points: vec![
12583                        FixedPoint {
12584                            point: point0_id,
12585                            position: Point2d {
12586                                x: Number {
12587                                    value: 2.0,
12588                                    units: NumericSuffix::Mm,
12589                                },
12590                                y: Number {
12591                                    value: 3.0,
12592                                    units: NumericSuffix::Mm,
12593                                },
12594                            },
12595                        },
12596                        FixedPoint {
12597                            point: point1_id,
12598                            position: Point2d {
12599                                x: Number {
12600                                    value: 4.0,
12601                                    units: NumericSuffix::Mm,
12602                                },
12603                                y: Number {
12604                                    value: 5.0,
12605                                    units: NumericSuffix::Mm,
12606                                },
12607                            },
12608                        },
12609                    ],
12610                }),
12611            )
12612            .await
12613            .unwrap();
12614        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12615        assert_eq!(
12616            scene_delta.new_graph.objects.len(),
12617            6,
12618            "{:#?}",
12619            scene_delta.new_graph.objects
12620        );
12621
12622        ctx.close().await;
12623        mock_ctx.close().await;
12624    }
12625
12626    #[tokio::test(flavor = "multi_thread")]
12627    async fn test_add_fixed_owned_point() {
12628        let initial_source = "\
12629sketch(on = XY) {
12630  line(start = [var 1, var 2], end = [var 3, var 4])
12631}
12632";
12633
12634        let program = Program::parse(initial_source).unwrap().0.unwrap();
12635
12636        let mut frontend = FrontendState::new();
12637
12638        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12639        let mock_ctx = ExecutorContext::new_mock(None).await;
12640        let version = Version(0);
12641
12642        frontend.hack_set_program(&ctx, program).await.unwrap();
12643        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12644        let sketch_id = sketch_object.id;
12645        let sketch = expect_sketch(sketch_object);
12646        let line_start_id = *sketch.segments.first().unwrap();
12647
12648        let (src_delta, scene_delta) = frontend
12649            .add_constraint(
12650                &mock_ctx,
12651                version,
12652                sketch_id,
12653                Constraint::Fixed(Fixed {
12654                    points: vec![FixedPoint {
12655                        point: line_start_id,
12656                        position: Point2d {
12657                            x: Number {
12658                                value: 2.0,
12659                                units: NumericSuffix::Mm,
12660                            },
12661                            y: Number {
12662                                value: 3.0,
12663                                units: NumericSuffix::Mm,
12664                            },
12665                        },
12666                    }],
12667                }),
12668            )
12669            .await
12670            .unwrap();
12671        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12672        assert_eq!(
12673            scene_delta.new_graph.objects.len(),
12674            6,
12675            "{:#?}",
12676            scene_delta.new_graph.objects
12677        );
12678
12679        ctx.close().await;
12680        mock_ctx.close().await;
12681    }
12682
12683    #[tokio::test(flavor = "multi_thread")]
12684    async fn test_radius_error_cases() {
12685        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12686        let mock_ctx = ExecutorContext::new_mock(None).await;
12687        let version = Version(0);
12688
12689        // Test: Single point should error
12690        let initial_source_point = "\
12691sketch(on = XY) {
12692  point(at = [var 1, var 2])
12693}
12694";
12695        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12696        let mut frontend_point = FrontendState::new();
12697        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12698        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12699        let sketch_id_point = sketch_object_point.id;
12700        let sketch_point = expect_sketch(sketch_object_point);
12701        let point_id = *sketch_point.segments.first().unwrap();
12702
12703        let constraint_point = Constraint::Radius(Radius {
12704            arc: point_id,
12705            radius: Number {
12706                value: 5.0,
12707                units: NumericSuffix::Mm,
12708            },
12709            label_position: None,
12710            source: Default::default(),
12711        });
12712        let result_point = frontend_point
12713            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12714            .await;
12715        assert!(result_point.is_err(), "Single point should error for radius");
12716
12717        // Test: Single line segment should error (only arc segments supported)
12718        let initial_source_line = "\
12719sketch(on = XY) {
12720  line(start = [var 1, var 2], end = [var 3, var 4])
12721}
12722";
12723        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12724        let mut frontend_line = FrontendState::new();
12725        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12726        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12727        let sketch_id_line = sketch_object_line.id;
12728        let sketch_line = expect_sketch(sketch_object_line);
12729        let line_id = *sketch_line.segments.first().unwrap();
12730
12731        let constraint_line = Constraint::Radius(Radius {
12732            arc: line_id,
12733            radius: Number {
12734                value: 5.0,
12735                units: NumericSuffix::Mm,
12736            },
12737            label_position: None,
12738            source: Default::default(),
12739        });
12740        let result_line = frontend_line
12741            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12742            .await;
12743        assert!(result_line.is_err(), "Single line segment should error for radius");
12744
12745        ctx.close().await;
12746        mock_ctx.close().await;
12747    }
12748
12749    #[tokio::test(flavor = "multi_thread")]
12750    async fn test_diameter_single_arc_segment() {
12751        let initial_source = "\
12752sketch(on = XY) {
12753  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12754}
12755";
12756
12757        let program = Program::parse(initial_source).unwrap().0.unwrap();
12758
12759        let mut frontend = FrontendState::new();
12760
12761        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12762        let mock_ctx = ExecutorContext::new_mock(None).await;
12763        let version = Version(0);
12764
12765        frontend.hack_set_program(&ctx, program).await.unwrap();
12766        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12767        let sketch_id = sketch_object.id;
12768        let sketch = expect_sketch(sketch_object);
12769        // Find the arc segment (not the points)
12770        let arc_id = sketch
12771            .segments
12772            .iter()
12773            .find(|&seg_id| {
12774                let obj = frontend.scene_graph.objects.get(seg_id.0);
12775                matches!(
12776                    obj.map(|o| &o.kind),
12777                    Some(ObjectKind::Segment {
12778                        segment: Segment::Arc(_)
12779                    })
12780                )
12781            })
12782            .unwrap();
12783
12784        let constraint = Constraint::Diameter(Diameter {
12785            arc: *arc_id,
12786            diameter: Number {
12787                value: 10.0,
12788                units: NumericSuffix::Mm,
12789            },
12790            label_position: None,
12791            source: Default::default(),
12792        });
12793        let (src_delta, scene_delta) = frontend
12794            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12795            .await
12796            .unwrap();
12797        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12798        assert_eq!(
12799            scene_delta.new_graph.objects.len(),
12800            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12801            "{:#?}",
12802            scene_delta.new_graph.objects
12803        );
12804
12805        ctx.close().await;
12806        mock_ctx.close().await;
12807    }
12808
12809    #[tokio::test(flavor = "multi_thread")]
12810    async fn test_diameter_single_arc_segment_with_label_position() {
12811        let initial_source = "\
12812sketch(on = XY) {
12813  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12814}
12815";
12816
12817        let program = Program::parse(initial_source).unwrap().0.unwrap();
12818        let mut frontend = FrontendState::new();
12819        let mock_ctx = ExecutorContext::new_mock(None).await;
12820        let version = Version(0);
12821
12822        frontend.program = program.clone();
12823        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12824        frontend.update_state_after_exec(outcome, true);
12825        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12826        let sketch_id = sketch_object.id;
12827        let sketch = expect_sketch(sketch_object);
12828        let arc_id = sketch
12829            .segments
12830            .iter()
12831            .find(|&seg_id| {
12832                let obj = frontend.scene_graph.objects.get(seg_id.0);
12833                matches!(
12834                    obj.map(|o| &o.kind),
12835                    Some(ObjectKind::Segment {
12836                        segment: Segment::Arc(_)
12837                    })
12838                )
12839            })
12840            .unwrap();
12841
12842        let label_position = Point2d {
12843            x: Number {
12844                value: 10.0,
12845                units: NumericSuffix::Mm,
12846            },
12847            y: Number {
12848                value: 11.0,
12849                units: NumericSuffix::Mm,
12850            },
12851        };
12852        let constraint = Constraint::Diameter(Diameter {
12853            arc: *arc_id,
12854            diameter: Number {
12855                value: 10.0,
12856                units: NumericSuffix::Mm,
12857            },
12858            label_position: Some(label_position.clone()),
12859            source: Default::default(),
12860        });
12861        let (src_delta, scene_delta) = frontend
12862            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12863            .await
12864            .unwrap();
12865        insta::assert_snapshot!(
12866            "test_diameter_single_arc_segment_with_label_position",
12867            src_delta.text.as_str()
12868        );
12869
12870        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12871        let sketch = expect_sketch(sketch_object);
12872        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12873        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12874            panic!("Expected constraint object");
12875        };
12876        let Constraint::Diameter(diameter) = constraint else {
12877            panic!("Expected diameter constraint");
12878        };
12879        assert_eq!(diameter.label_position, Some(label_position));
12880
12881        mock_ctx.close().await;
12882    }
12883
12884    #[tokio::test(flavor = "multi_thread")]
12885    async fn test_edit_diameter_constraint_label_position() {
12886        let initial_source = "\
12887sketch(on = XY) {
12888  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12889  diameter(arc1) == 10mm
12890}
12891";
12892
12893        let program = Program::parse(initial_source).unwrap().0.unwrap();
12894        let mut frontend = FrontendState::new();
12895        let mock_ctx = ExecutorContext::new_mock(None).await;
12896        let version = Version(0);
12897
12898        frontend.program = program.clone();
12899        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12900        frontend.update_state_after_exec(outcome, true);
12901        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12902        let sketch_id = sketch_object.id;
12903        let sketch = expect_sketch(sketch_object);
12904        let constraint_id = sketch.constraints[0];
12905        let label_position = Point2d {
12906            x: Number {
12907                value: 10.0,
12908                units: NumericSuffix::Mm,
12909            },
12910            y: Number {
12911                value: 11.0,
12912                units: NumericSuffix::Mm,
12913            },
12914        };
12915
12916        let (src_delta, scene_delta) = frontend
12917            .edit_distance_constraint_label_position(
12918                &mock_ctx,
12919                version,
12920                sketch_id,
12921                constraint_id,
12922                label_position.clone(),
12923                vec![],
12924            )
12925            .await
12926            .unwrap();
12927        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12928
12929        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12930        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12931            panic!("Expected constraint object");
12932        };
12933        let Constraint::Diameter(diameter) = constraint else {
12934            panic!("Expected diameter constraint");
12935        };
12936        assert_eq!(diameter.label_position, Some(label_position));
12937
12938        mock_ctx.close().await;
12939    }
12940
12941    #[tokio::test(flavor = "multi_thread")]
12942    async fn test_diameter_error_cases() {
12943        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12944        let mock_ctx = ExecutorContext::new_mock(None).await;
12945        let version = Version(0);
12946
12947        // Test: Single point should error
12948        let initial_source_point = "\
12949sketch(on = XY) {
12950  point(at = [var 1, var 2])
12951}
12952";
12953        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12954        let mut frontend_point = FrontendState::new();
12955        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12956        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12957        let sketch_id_point = sketch_object_point.id;
12958        let sketch_point = expect_sketch(sketch_object_point);
12959        let point_id = *sketch_point.segments.first().unwrap();
12960
12961        let constraint_point = Constraint::Diameter(Diameter {
12962            arc: point_id,
12963            diameter: Number {
12964                value: 10.0,
12965                units: NumericSuffix::Mm,
12966            },
12967            label_position: None,
12968            source: Default::default(),
12969        });
12970        let result_point = frontend_point
12971            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12972            .await;
12973        assert!(result_point.is_err(), "Single point should error for diameter");
12974
12975        // Test: Single line segment should error (only arc segments supported)
12976        let initial_source_line = "\
12977sketch(on = XY) {
12978  line(start = [var 1, var 2], end = [var 3, var 4])
12979}
12980";
12981        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12982        let mut frontend_line = FrontendState::new();
12983        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12984        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12985        let sketch_id_line = sketch_object_line.id;
12986        let sketch_line = expect_sketch(sketch_object_line);
12987        let line_id = *sketch_line.segments.first().unwrap();
12988
12989        let constraint_line = Constraint::Diameter(Diameter {
12990            arc: line_id,
12991            diameter: Number {
12992                value: 10.0,
12993                units: NumericSuffix::Mm,
12994            },
12995            label_position: None,
12996            source: Default::default(),
12997        });
12998        let result_line = frontend_line
12999            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13000            .await;
13001        assert!(result_line.is_err(), "Single line segment should error for diameter");
13002
13003        ctx.close().await;
13004        mock_ctx.close().await;
13005    }
13006
13007    #[tokio::test(flavor = "multi_thread")]
13008    async fn test_line_horizontal() {
13009        let initial_source = "\
13010sketch(on = XY) {
13011  line(start = [var 1, var 2], end = [var 3, var 4])
13012}
13013";
13014
13015        let program = Program::parse(initial_source).unwrap().0.unwrap();
13016
13017        let mut frontend = FrontendState::new();
13018
13019        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13020        let mock_ctx = ExecutorContext::new_mock(None).await;
13021        let version = Version(0);
13022
13023        frontend.hack_set_program(&ctx, program).await.unwrap();
13024        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13025        let sketch_id = sketch_object.id;
13026        let sketch = expect_sketch(sketch_object);
13027        let line1_id = *sketch.segments.get(2).unwrap();
13028
13029        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13030        let (src_delta, scene_delta) = frontend
13031            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13032            .await
13033            .unwrap();
13034        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13035        assert_eq!(
13036            scene_delta.new_graph.objects.len(),
13037            6,
13038            "{:#?}",
13039            scene_delta.new_graph.objects
13040        );
13041
13042        ctx.close().await;
13043        mock_ctx.close().await;
13044    }
13045
13046    #[tokio::test(flavor = "multi_thread")]
13047    async fn test_control_point_spline_edge_horizontal() {
13048        let initial_source = "\
13049@settings(experimentalFeatures = allow)
13050splineSketch = sketch(on = XY) {
13051  controlPointSpline1 = controlPointSpline(points = [
13052    [var 0mm, var 0mm],
13053    [var 10mm, var 20mm],
13054    [var 20mm, var 0mm],
13055  ])
13056}
13057";
13058
13059        let program = Program::parse(initial_source).unwrap().0.unwrap();
13060
13061        let mut frontend = FrontendState::new();
13062
13063        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13064        let mock_ctx = ExecutorContext::new_mock(None).await;
13065        let version = Version(0);
13066
13067        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13069        let sketch_id = sketch_object.id;
13070        let sketch = expect_sketch(sketch_object);
13071        let spline_id = sketch
13072            .segments
13073            .iter()
13074            .copied()
13075            .find(|seg_id| {
13076                matches!(
13077                    &frontend.scene_graph.objects[seg_id.0].kind,
13078                    ObjectKind::Segment {
13079                        segment: Segment::ControlPointSpline(_)
13080                    }
13081                )
13082            })
13083            .expect("Expected a control point spline segment in sketch");
13084        let edge_id = frontend
13085            .scene_graph
13086            .objects
13087            .iter()
13088            .find_map(|obj| match &obj.kind {
13089                ObjectKind::Segment {
13090                    segment: Segment::Line(line),
13091                } if line.owner == Some(spline_id) => Some(obj.id),
13092                _ => None,
13093            })
13094            .expect("Expected an owned control-polygon edge");
13095
13096        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13097        let (src_delta, _) = frontend
13098            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13099            .await
13100            .unwrap();
13101        assert!(
13102            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13103            "Expected horizontal constraint on spline edge, got: {}",
13104            src_delta.text
13105        );
13106
13107        ctx.close().await;
13108        mock_ctx.close().await;
13109    }
13110
13111    #[tokio::test(flavor = "multi_thread")]
13112    async fn test_control_point_spline_edge_angle() {
13113        let initial_source = "\
13114@settings(experimentalFeatures = allow)
13115splineSketch = sketch(on = XY) {
13116  controlPointSpline1 = controlPointSpline(points = [
13117    [var 0mm, var 0mm],
13118    [var 10mm, var 20mm],
13119    [var 20mm, var 0mm],
13120  ])
13121
13122  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13123}
13124";
13125
13126        let program = Program::parse(initial_source).unwrap().0.unwrap();
13127
13128        let mut frontend = FrontendState::new();
13129
13130        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13131        let mock_ctx = ExecutorContext::new_mock(None).await;
13132        let version = Version(0);
13133
13134        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13135        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13136        let sketch_id = sketch_object.id;
13137        let sketch = expect_sketch(sketch_object);
13138        let spline_id = sketch
13139            .segments
13140            .iter()
13141            .copied()
13142            .find(|seg_id| {
13143                matches!(
13144                    &frontend.scene_graph.objects[seg_id.0].kind,
13145                    ObjectKind::Segment {
13146                        segment: Segment::ControlPointSpline(_)
13147                    }
13148                )
13149            })
13150            .expect("Expected a control point spline segment in sketch");
13151        let edge_id = frontend
13152            .scene_graph
13153            .objects
13154            .iter()
13155            .find_map(|obj| match &obj.kind {
13156                ObjectKind::Segment {
13157                    segment: Segment::Line(line),
13158                } if line.owner == Some(spline_id) => Some(obj.id),
13159                _ => None,
13160            })
13161            .expect("Expected an owned control-polygon edge");
13162        let line1_id = frontend
13163            .scene_graph
13164            .objects
13165            .iter()
13166            .find_map(|obj| match &obj.kind {
13167                ObjectKind::Segment {
13168                    segment: Segment::Line(line),
13169                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13170                _ => None,
13171            })
13172            .or_else(|| {
13173                sketch.segments.iter().copied().find(|seg_id| {
13174                    matches!(
13175                        &frontend.scene_graph.objects[seg_id.0].kind,
13176                        ObjectKind::Segment {
13177                            segment: Segment::Line(line),
13178                        } if line.owner.is_none()
13179                    )
13180                })
13181            })
13182            .expect("Expected a standalone line segment in sketch");
13183
13184        let constraint = Constraint::Angle(Angle {
13185            lines: vec![line1_id, edge_id],
13186            angle: Number {
13187                value: 30.0,
13188                units: NumericSuffix::Deg,
13189            },
13190            sector: None,
13191            inverse: None,
13192            label_position: None,
13193            source: Default::default(),
13194        });
13195        let (src_delta, _) = frontend
13196            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13197            .await
13198            .unwrap();
13199        assert!(
13200            src_delta
13201                .text
13202                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13203            "Expected angle constraint on spline edge, got: {}",
13204            src_delta.text
13205        );
13206
13207        ctx.close().await;
13208        mock_ctx.close().await;
13209    }
13210
13211    #[tokio::test(flavor = "multi_thread")]
13212    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13213        let initial_source = "\
13214@settings(experimentalFeatures = allow)
13215splineSketch = sketch(on = XY) {
13216  spline1 = controlPointSpline(points = [
13217    [var 0mm, var 0mm],
13218    [var 10mm, var 20mm],
13219    [var 20mm, var 0mm],
13220  ])
13221  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13222  coincident([spline1.controls[1], spline1.edges[0]])
13223  coincident([spline1.controls[0], line1])
13224}
13225";
13226
13227        let program = Program::parse(initial_source).unwrap().0.unwrap();
13228
13229        let mut frontend = FrontendState::new();
13230
13231        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13232        let mock_ctx = ExecutorContext::new_mock(None).await;
13233
13234        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13235
13236        let ui_scene_graph = frontend.scene_graph_for_ui();
13237        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13238        let sketch = expect_sketch(sketch_object);
13239
13240        assert_eq!(
13241            sketch.constraints.len(),
13242            1,
13243            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13244        );
13245
13246        let visible_constraints = ui_scene_graph
13247            .objects
13248            .iter()
13249            .filter_map(|object| match &object.kind {
13250                ObjectKind::Constraint {
13251                    constraint: Constraint::Coincident(coincident),
13252                } => Some(coincident.clone()),
13253                _ => None,
13254            })
13255            .collect::<Vec<_>>();
13256
13257        assert_eq!(
13258            visible_constraints.len(),
13259            1,
13260            "Expected only one coincident constraint object in the UI scene graph"
13261        );
13262        assert_eq!(
13263            visible_constraints[0].get_segments().len(),
13264            2,
13265            "Expected the remaining visible coincident constraint to reference two segments"
13266        );
13267
13268        ctx.close().await;
13269        mock_ctx.close().await;
13270    }
13271
13272    #[tokio::test(flavor = "multi_thread")]
13273    async fn test_edit_control_point_spline_can_append_control_point() {
13274        let initial_source = "\
13275@settings(experimentalFeatures = allow)
13276splineSketch = sketch(on = XY) {
13277  controlPointSpline(points = [
13278    [var 0mm, var 0mm],
13279    [var 10mm, var 20mm],
13280    [var 20mm, var 0mm],
13281  ])
13282}
13283";
13284
13285        let program = Program::parse(initial_source).unwrap().0.unwrap();
13286
13287        let mut frontend = FrontendState::new();
13288
13289        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13290        let mock_ctx = ExecutorContext::new_mock(None).await;
13291        let version = Version(0);
13292
13293        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13294        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13295        let sketch_id = sketch_object.id;
13296        let sketch = expect_sketch(sketch_object);
13297        let spline_id = sketch
13298            .segments
13299            .iter()
13300            .copied()
13301            .find(|seg_id| {
13302                matches!(
13303                    &frontend.scene_graph.objects[seg_id.0].kind,
13304                    ObjectKind::Segment {
13305                        segment: Segment::ControlPointSpline(_)
13306                    }
13307                )
13308            })
13309            .expect("Expected a control point spline segment in sketch");
13310
13311        let ctor = ControlPointSplineCtor {
13312            points: vec![
13313                Point2d {
13314                    x: Expr::Var(Number {
13315                        value: 0.0,
13316                        units: NumericSuffix::Mm,
13317                    }),
13318                    y: Expr::Var(Number {
13319                        value: 0.0,
13320                        units: NumericSuffix::Mm,
13321                    }),
13322                },
13323                Point2d {
13324                    x: Expr::Var(Number {
13325                        value: 10.0,
13326                        units: NumericSuffix::Mm,
13327                    }),
13328                    y: Expr::Var(Number {
13329                        value: 20.0,
13330                        units: NumericSuffix::Mm,
13331                    }),
13332                },
13333                Point2d {
13334                    x: Expr::Var(Number {
13335                        value: 20.0,
13336                        units: NumericSuffix::Mm,
13337                    }),
13338                    y: Expr::Var(Number {
13339                        value: 0.0,
13340                        units: NumericSuffix::Mm,
13341                    }),
13342                },
13343                Point2d {
13344                    x: Expr::Var(Number {
13345                        value: 30.0,
13346                        units: NumericSuffix::Mm,
13347                    }),
13348                    y: Expr::Var(Number {
13349                        value: 10.0,
13350                        units: NumericSuffix::Mm,
13351                    }),
13352                },
13353            ],
13354            construction: None,
13355        };
13356
13357        let segments = vec![ExistingSegmentCtor {
13358            id: spline_id,
13359            ctor: SegmentCtor::ControlPointSpline(ctor),
13360        }];
13361        let (src_delta, scene_delta) = frontend
13362            .edit_segments(&mock_ctx, version, sketch_id, segments)
13363            .await
13364            .unwrap();
13365
13366        assert!(
13367            src_delta.text.contains("[var 30mm, var 10mm]"),
13368            "Expected appended spline control point in source, got: {}",
13369            src_delta.text
13370        );
13371
13372        assert!(
13373            scene_delta.invalidates_ids,
13374            "Expected appending a spline control point to invalidate ids"
13375        );
13376        let updated_spline = scene_delta
13377            .new_graph
13378            .objects
13379            .iter()
13380            .find_map(|obj| match &obj.kind {
13381                ObjectKind::Segment {
13382                    segment: Segment::ControlPointSpline(updated_spline),
13383                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13384                _ => None,
13385            })
13386            .expect("Expected edited scene graph to contain a four-point control point spline");
13387        assert_eq!(
13388            updated_spline.controls.len(),
13389            4,
13390            "Expected edited spline to expose four control points"
13391        );
13392
13393        ctx.close().await;
13394        mock_ctx.close().await;
13395    }
13396
13397    #[tokio::test(flavor = "multi_thread")]
13398    async fn test_line_vertical() {
13399        let initial_source = "\
13400sketch(on = XY) {
13401  line(start = [var 1, var 2], end = [var 3, var 4])
13402}
13403";
13404
13405        let program = Program::parse(initial_source).unwrap().0.unwrap();
13406
13407        let mut frontend = FrontendState::new();
13408
13409        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13410        let mock_ctx = ExecutorContext::new_mock(None).await;
13411        let version = Version(0);
13412
13413        frontend.hack_set_program(&ctx, program).await.unwrap();
13414        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13415        let sketch_id = sketch_object.id;
13416        let sketch = expect_sketch(sketch_object);
13417        let line1_id = *sketch.segments.get(2).unwrap();
13418
13419        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13420        let (src_delta, scene_delta) = frontend
13421            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13422            .await
13423            .unwrap();
13424        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13425        assert_eq!(
13426            scene_delta.new_graph.objects.len(),
13427            6,
13428            "{:#?}",
13429            scene_delta.new_graph.objects
13430        );
13431
13432        ctx.close().await;
13433        mock_ctx.close().await;
13434    }
13435
13436    #[tokio::test(flavor = "multi_thread")]
13437    async fn test_points_vertical() {
13438        let initial_source = "\
13439sketch001 = sketch(on = XY) {
13440  p0 = point(at = [var -2.23mm, var 3.1mm])
13441  pf = point(at = [4, 4])
13442}
13443";
13444
13445        let program = Program::parse(initial_source).unwrap().0.unwrap();
13446
13447        let mut frontend = FrontendState::new();
13448
13449        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13450        let mock_ctx = ExecutorContext::new_mock(None).await;
13451        let version = Version(0);
13452
13453        frontend.hack_set_program(&ctx, program).await.unwrap();
13454        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13455        let sketch_id = sketch_object.id;
13456        let sketch = expect_sketch(sketch_object);
13457        let point_ids = vec![
13458            sketch.segments.first().unwrap().to_owned(),
13459            sketch.segments.get(1).unwrap().to_owned(),
13460        ];
13461
13462        let constraint = Constraint::Vertical(Vertical::Points {
13463            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13464        });
13465        let (src_delta, scene_delta) = frontend
13466            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13467            .await
13468            .unwrap();
13469        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13470        assert_eq!(
13471            scene_delta.new_graph.objects.len(),
13472            5,
13473            "{:#?}",
13474            scene_delta.new_graph.objects
13475        );
13476
13477        ctx.close().await;
13478        mock_ctx.close().await;
13479    }
13480
13481    #[tokio::test(flavor = "multi_thread")]
13482    async fn test_points_horizontal() {
13483        let initial_source = "\
13484sketch001 = sketch(on = XY) {
13485  p0 = point(at = [var -2.23mm, var 3.1mm])
13486  pf = point(at = [4, 4])
13487}
13488";
13489
13490        let program = Program::parse(initial_source).unwrap().0.unwrap();
13491
13492        let mut frontend = FrontendState::new();
13493
13494        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13495        let mock_ctx = ExecutorContext::new_mock(None).await;
13496        let version = Version(0);
13497
13498        frontend.hack_set_program(&ctx, program).await.unwrap();
13499        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13500        let sketch_id = sketch_object.id;
13501        let sketch = expect_sketch(sketch_object);
13502        let point_ids = vec![
13503            sketch.segments.first().unwrap().to_owned(),
13504            sketch.segments.get(1).unwrap().to_owned(),
13505        ];
13506
13507        let constraint = Constraint::Horizontal(Horizontal::Points {
13508            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13509        });
13510        let (src_delta, scene_delta) = frontend
13511            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13512            .await
13513            .unwrap();
13514        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13515        assert_eq!(
13516            scene_delta.new_graph.objects.len(),
13517            5,
13518            "{:#?}",
13519            scene_delta.new_graph.objects
13520        );
13521
13522        ctx.close().await;
13523        mock_ctx.close().await;
13524    }
13525
13526    #[tokio::test(flavor = "multi_thread")]
13527    async fn test_point_horizontal_with_origin() {
13528        let initial_source = "\
13529sketch001 = sketch(on = XY) {
13530  p0 = point(at = [var -2.23mm, var 3.1mm])
13531}
13532";
13533
13534        let program = Program::parse(initial_source).unwrap().0.unwrap();
13535
13536        let mut frontend = FrontendState::new();
13537
13538        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13539        let mock_ctx = ExecutorContext::new_mock(None).await;
13540        let version = Version(0);
13541
13542        frontend.hack_set_program(&ctx, program).await.unwrap();
13543        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13544        let sketch_id = sketch_object.id;
13545        let sketch = expect_sketch(sketch_object);
13546        let point_id = *sketch.segments.first().unwrap();
13547
13548        let constraint = Constraint::Horizontal(Horizontal::Points {
13549            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13550        });
13551        let (src_delta, scene_delta) = frontend
13552            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13553            .await
13554            .unwrap();
13555        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13556        assert_eq!(
13557            scene_delta.new_graph.objects.len(),
13558            4,
13559            "{:#?}",
13560            scene_delta.new_graph.objects
13561        );
13562
13563        ctx.close().await;
13564        mock_ctx.close().await;
13565    }
13566
13567    #[tokio::test(flavor = "multi_thread")]
13568    async fn test_lines_equal_length() {
13569        let initial_source = "\
13570sketch(on = XY) {
13571  line(start = [var 1, var 2], end = [var 3, var 4])
13572  line(start = [var 5, var 6], end = [var 7, var 8])
13573}
13574";
13575
13576        let program = Program::parse(initial_source).unwrap().0.unwrap();
13577
13578        let mut frontend = FrontendState::new();
13579
13580        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13581        let mock_ctx = ExecutorContext::new_mock(None).await;
13582        let version = Version(0);
13583
13584        frontend.hack_set_program(&ctx, program).await.unwrap();
13585        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13586        let sketch_id = sketch_object.id;
13587        let sketch = expect_sketch(sketch_object);
13588        let line1_id = *sketch.segments.get(2).unwrap();
13589        let line2_id = *sketch.segments.get(5).unwrap();
13590
13591        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13592            lines: vec![line1_id, line2_id],
13593        });
13594        let (src_delta, scene_delta) = frontend
13595            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13596            .await
13597            .unwrap();
13598        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13599        assert_eq!(
13600            scene_delta.new_graph.objects.len(),
13601            9,
13602            "{:#?}",
13603            scene_delta.new_graph.objects
13604        );
13605
13606        ctx.close().await;
13607        mock_ctx.close().await;
13608    }
13609
13610    #[tokio::test(flavor = "multi_thread")]
13611    async fn test_add_constraint_multi_line_equal_length() {
13612        let initial_source = "\
13613sketch(on = XY) {
13614  line(start = [var 1, var 2], end = [var 3, var 4])
13615  line(start = [var 5, var 6], end = [var 7, var 8])
13616  line(start = [var 9, var 10], end = [var 11, var 12])
13617}
13618";
13619
13620        let program = Program::parse(initial_source).unwrap().0.unwrap();
13621
13622        let mut frontend = FrontendState::new();
13623        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13624        let mock_ctx = ExecutorContext::new_mock(None).await;
13625        let version = Version(0);
13626
13627        frontend.hack_set_program(&ctx, program).await.unwrap();
13628        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13629        let sketch_id = sketch_object.id;
13630        let sketch = expect_sketch(sketch_object);
13631        let line1_id = *sketch.segments.get(2).unwrap();
13632        let line2_id = *sketch.segments.get(5).unwrap();
13633        let line3_id = *sketch.segments.get(8).unwrap();
13634
13635        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13636            lines: vec![line1_id, line2_id, line3_id],
13637        });
13638        let (src_delta, scene_delta) = frontend
13639            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13640            .await
13641            .unwrap();
13642        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13643        let constraints = scene_delta
13644            .new_graph
13645            .objects
13646            .iter()
13647            .filter_map(|obj| {
13648                let ObjectKind::Constraint { constraint } = &obj.kind else {
13649                    return None;
13650                };
13651                Some(constraint)
13652            })
13653            .collect::<Vec<_>>();
13654
13655        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13656        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13657            panic!("expected equal length constraint, got {:?}", constraints[0]);
13658        };
13659        assert_eq!(lines_equal_length.lines.len(), 3);
13660
13661        ctx.close().await;
13662        mock_ctx.close().await;
13663    }
13664
13665    #[tokio::test(flavor = "multi_thread")]
13666    async fn test_lines_parallel() {
13667        let initial_source = "\
13668sketch(on = XY) {
13669  line(start = [var 1, var 2], end = [var 3, var 4])
13670  line(start = [var 5, var 6], end = [var 7, var 8])
13671}
13672";
13673
13674        let program = Program::parse(initial_source).unwrap().0.unwrap();
13675
13676        let mut frontend = FrontendState::new();
13677
13678        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13679        let mock_ctx = ExecutorContext::new_mock(None).await;
13680        let version = Version(0);
13681
13682        frontend.hack_set_program(&ctx, program).await.unwrap();
13683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13684        let sketch_id = sketch_object.id;
13685        let sketch = expect_sketch(sketch_object);
13686        let line1_id = *sketch.segments.get(2).unwrap();
13687        let line2_id = *sketch.segments.get(5).unwrap();
13688
13689        let constraint = Constraint::Parallel(Parallel {
13690            lines: vec![line1_id, line2_id],
13691        });
13692        let (src_delta, scene_delta) = frontend
13693            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13694            .await
13695            .unwrap();
13696        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13697        assert_eq!(
13698            scene_delta.new_graph.objects.len(),
13699            9,
13700            "{:#?}",
13701            scene_delta.new_graph.objects
13702        );
13703
13704        ctx.close().await;
13705        mock_ctx.close().await;
13706    }
13707
13708    #[tokio::test(flavor = "multi_thread")]
13709    async fn test_lines_parallel_multiline() {
13710        let initial_source = "\
13711sketch(on = XY) {
13712  line(start = [var 1, var 2], end = [var 3, var 4])
13713  line(start = [var 5, var 6], end = [var 7, var 8])
13714  line(start = [var 9, var 10], end = [var 11, var 12])
13715}
13716";
13717
13718        let program = Program::parse(initial_source).unwrap().0.unwrap();
13719
13720        let mut frontend = FrontendState::new();
13721
13722        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13723        let mock_ctx = ExecutorContext::new_mock(None).await;
13724        let version = Version(0);
13725
13726        frontend.hack_set_program(&ctx, program).await.unwrap();
13727        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13728        let sketch_id = sketch_object.id;
13729        let sketch = expect_sketch(sketch_object);
13730        let line1_id = *sketch.segments.get(2).unwrap();
13731        let line2_id = *sketch.segments.get(5).unwrap();
13732        let line3_id = *sketch.segments.get(8).unwrap();
13733
13734        let constraint = Constraint::Parallel(Parallel {
13735            lines: vec![line1_id, line2_id, line3_id],
13736        });
13737        let (src_delta, scene_delta) = frontend
13738            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13739            .await
13740            .unwrap();
13741        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13742
13743        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13744        let sketch = expect_sketch(sketch_object);
13745        assert_eq!(sketch.constraints.len(), 1);
13746
13747        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13748        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13749            panic!("Expected constraint object");
13750        };
13751        let Constraint::Parallel(parallel) = constraint else {
13752            panic!("Expected parallel constraint");
13753        };
13754        assert_eq!(parallel.lines.len(), 3);
13755
13756        ctx.close().await;
13757        mock_ctx.close().await;
13758    }
13759
13760    #[tokio::test(flavor = "multi_thread")]
13761    async fn test_lines_perpendicular() {
13762        let initial_source = "\
13763sketch(on = XY) {
13764  line(start = [var 1, var 2], end = [var 3, var 4])
13765  line(start = [var 5, var 6], end = [var 7, var 8])
13766}
13767";
13768
13769        let program = Program::parse(initial_source).unwrap().0.unwrap();
13770
13771        let mut frontend = FrontendState::new();
13772
13773        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13774        let mock_ctx = ExecutorContext::new_mock(None).await;
13775        let version = Version(0);
13776
13777        frontend.hack_set_program(&ctx, program).await.unwrap();
13778        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13779        let sketch_id = sketch_object.id;
13780        let sketch = expect_sketch(sketch_object);
13781        let line1_id = *sketch.segments.get(2).unwrap();
13782        let line2_id = *sketch.segments.get(5).unwrap();
13783
13784        let constraint = Constraint::Perpendicular(Perpendicular {
13785            lines: vec![line1_id, line2_id],
13786        });
13787        let (src_delta, scene_delta) = frontend
13788            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13789            .await
13790            .unwrap();
13791        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13792        assert_eq!(
13793            scene_delta.new_graph.objects.len(),
13794            9,
13795            "{:#?}",
13796            scene_delta.new_graph.objects
13797        );
13798
13799        ctx.close().await;
13800        mock_ctx.close().await;
13801    }
13802
13803    #[tokio::test(flavor = "multi_thread")]
13804    async fn test_lines_angle() {
13805        let initial_source = "\
13806sketch(on = XY) {
13807  line(start = [var 1, var 2], end = [var 3, var 4])
13808  line(start = [var 5, var 6], end = [var 7, var 8])
13809}
13810";
13811
13812        let program = Program::parse(initial_source).unwrap().0.unwrap();
13813
13814        let mut frontend = FrontendState::new();
13815
13816        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13817        let mock_ctx = ExecutorContext::new_mock(None).await;
13818        let version = Version(0);
13819
13820        frontend.hack_set_program(&ctx, program).await.unwrap();
13821        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13822        let sketch_id = sketch_object.id;
13823        let sketch = expect_sketch(sketch_object);
13824        let line1_id = *sketch.segments.get(2).unwrap();
13825        let line2_id = *sketch.segments.get(5).unwrap();
13826
13827        let constraint = Constraint::Angle(Angle {
13828            lines: vec![line1_id, line2_id],
13829            angle: Number {
13830                value: 30.0,
13831                units: NumericSuffix::Deg,
13832            },
13833            sector: None,
13834            inverse: None,
13835            label_position: None,
13836            source: Default::default(),
13837        });
13838        let (src_delta, scene_delta) = frontend
13839            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13840            .await
13841            .unwrap();
13842        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13843        assert_eq!(
13844            scene_delta.new_graph.objects.len(),
13845            9,
13846            "{:#?}",
13847            scene_delta.new_graph.objects
13848        );
13849
13850        ctx.close().await;
13851        mock_ctx.close().await;
13852    }
13853
13854    #[tokio::test(flavor = "multi_thread")]
13855    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13856        let initial_source = "\
13857sketch(on = XY) {
13858  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13859  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13860}
13861";
13862
13863        let program = Program::parse(initial_source).unwrap().0.unwrap();
13864
13865        let mut frontend = FrontendState::new();
13866
13867        let mock_ctx = ExecutorContext::new_mock(None).await;
13868        let version = Version(0);
13869
13870        frontend.program = program.clone();
13871        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13872        frontend.update_state_after_exec(outcome, true);
13873        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13874        let sketch_id = sketch_object.id;
13875        let sketch = expect_sketch(sketch_object);
13876        let line1_id = *sketch.segments.get(2).unwrap();
13877        let line2_id = *sketch.segments.get(5).unwrap();
13878
13879        let constraint = Constraint::Angle(Angle {
13880            lines: vec![line1_id, line2_id],
13881            angle: Number {
13882                value: 270.0,
13883                units: NumericSuffix::Deg,
13884            },
13885            sector: Some(1),
13886            inverse: Some(true),
13887            label_position: Some(Point2d {
13888                x: Number {
13889                    value: -0.73,
13890                    units: NumericSuffix::Mm,
13891                },
13892                y: Number {
13893                    value: 0.75,
13894                    units: NumericSuffix::Mm,
13895                },
13896            }),
13897            source: Default::default(),
13898        });
13899        let (src_delta, _) = frontend
13900            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13901            .await
13902            .unwrap();
13903        assert_eq!(
13904            src_delta.text.as_str(),
13905            "\
13906sketch(on = XY) {
13907  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13908  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13909  angleDimension(
13910  lines = [line1, line2],
13911  sector = 1,
13912  inverse = true,
13913  labelPosition = [-0.73mm, 0.75mm],
13914) == 270deg
13915}
13916"
13917        );
13918
13919        mock_ctx.close().await;
13920    }
13921
13922    #[tokio::test(flavor = "multi_thread")]
13923    async fn test_segments_tangent() {
13924        let initial_source = "\
13925sketch(on = XY) {
13926  line(start = [var 1, var 2], end = [var 3, var 4])
13927  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13928}
13929";
13930
13931        let program = Program::parse(initial_source).unwrap().0.unwrap();
13932
13933        let mut frontend = FrontendState::new();
13934
13935        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13936        let mock_ctx = ExecutorContext::new_mock(None).await;
13937        let version = Version(0);
13938
13939        frontend.hack_set_program(&ctx, program).await.unwrap();
13940        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13941        let sketch_id = sketch_object.id;
13942        let sketch = expect_sketch(sketch_object);
13943        let line1_id = *sketch.segments.get(2).unwrap();
13944        let arc1_id = *sketch.segments.get(6).unwrap();
13945
13946        let constraint = Constraint::Tangent(Tangent {
13947            input: vec![line1_id, arc1_id],
13948        });
13949        let (src_delta, scene_delta) = frontend
13950            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13951            .await
13952            .unwrap();
13953        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13954        assert_eq!(
13955            scene_delta.new_graph.objects.len(),
13956            10,
13957            "{:#?}",
13958            scene_delta.new_graph.objects
13959        );
13960
13961        ctx.close().await;
13962        mock_ctx.close().await;
13963    }
13964
13965    #[tokio::test(flavor = "multi_thread")]
13966    async fn test_point_midpoint() {
13967        let initial_source = "\
13968sketch(on = XY) {
13969  point(at = [var 1, var 1])
13970  line(start = [var 0, var 0], end = [var 6, var 4])
13971}
13972";
13973
13974        let program = Program::parse(initial_source).unwrap().0.unwrap();
13975
13976        let mut frontend = FrontendState::new();
13977
13978        let ctx = ExecutorContext::new_mock(None).await;
13979        let version = Version(0);
13980
13981        frontend.program = program.clone();
13982        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13983        frontend.update_state_after_exec(outcome, true);
13984        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13985        let sketch_id = sketch_object.id;
13986        let sketch = expect_sketch(sketch_object);
13987        let point_id = *sketch.segments.first().unwrap();
13988        let line_id = *sketch.segments.get(3).unwrap();
13989
13990        let constraint = Constraint::Midpoint(Midpoint {
13991            point: ConstraintSegment::from(point_id),
13992            segment: line_id,
13993        });
13994        let (src_delta, scene_delta) = frontend
13995            .add_constraint(&ctx, version, sketch_id, constraint)
13996            .await
13997            .unwrap();
13998        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13999        assert_eq!(
14000            scene_delta.new_graph.objects.len(),
14001            7,
14002            "{:#?}",
14003            scene_delta.new_graph.objects
14004        );
14005
14006        ctx.close().await;
14007    }
14008
14009    #[tokio::test(flavor = "multi_thread")]
14010    async fn test_segments_symmetric() {
14011        let initial_source = "\
14012sketch(on = XY) {
14013  line(start = [var 0, var 0], end = [var 0, var 4])
14014  line(start = [var 4, var 0], end = [var 4, var 4])
14015  line(start = [var 2, var -1], end = [var 2, var 5])
14016}
14017";
14018
14019        let program = Program::parse(initial_source).unwrap().0.unwrap();
14020
14021        let mut frontend = FrontendState::new();
14022
14023        let ctx = ExecutorContext::new_mock(None).await;
14024        let version = Version(0);
14025
14026        frontend.program = program.clone();
14027        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14028        frontend.update_state_after_exec(outcome, true);
14029        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14030        let sketch_id = sketch_object.id;
14031        let sketch = expect_sketch(sketch_object);
14032        let line1_id = *sketch.segments.get(2).unwrap();
14033        let line2_id = *sketch.segments.get(5).unwrap();
14034        let axis_id = *sketch.segments.get(8).unwrap();
14035
14036        let constraint = Constraint::Symmetric(Symmetric {
14037            input: vec![line1_id, line2_id],
14038            axis: axis_id,
14039        });
14040        let (src_delta, scene_delta) = frontend
14041            .add_constraint(&ctx, version, sketch_id, constraint)
14042            .await
14043            .unwrap();
14044        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14045        assert_eq!(
14046            scene_delta.new_graph.objects.len(),
14047            12,
14048            "{:#?}",
14049            scene_delta.new_graph.objects
14050        );
14051
14052        ctx.close().await;
14053    }
14054
14055    #[tokio::test(flavor = "multi_thread")]
14056    async fn test_point_arc_midpoint() {
14057        let initial_source = "\
14058sketch(on = XY) {
14059  point(at = [var 6, var 3])
14060  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14061}
14062";
14063
14064        let program = Program::parse(initial_source).unwrap().0.unwrap();
14065
14066        let mut frontend = FrontendState::new();
14067
14068        let ctx = ExecutorContext::new_mock(None).await;
14069        let version = Version(0);
14070
14071        frontend.program = program.clone();
14072        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14073        frontend.update_state_after_exec(outcome, true);
14074        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14075        let sketch_id = sketch_object.id;
14076        let sketch = expect_sketch(sketch_object);
14077        let point_id = *sketch.segments.first().unwrap();
14078        let arc_id = *sketch.segments.get(4).unwrap();
14079
14080        let constraint = Constraint::Midpoint(Midpoint {
14081            point: ConstraintSegment::from(point_id),
14082            segment: arc_id,
14083        });
14084        let (src_delta, scene_delta) = frontend
14085            .add_constraint(&ctx, version, sketch_id, constraint)
14086            .await
14087            .unwrap();
14088        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14089        assert_eq!(
14090            scene_delta.new_graph.objects.len(),
14091            8,
14092            "{:#?}",
14093            scene_delta.new_graph.objects
14094        );
14095
14096        ctx.close().await;
14097    }
14098
14099    #[tokio::test(flavor = "multi_thread")]
14100    async fn test_origin_line_midpoint() {
14101        let initial_source = "\
14102sketch(on = XY) {
14103  line(start = [var 0, var 0], end = [var 6, var 4])
14104}
14105";
14106
14107        let program = Program::parse(initial_source).unwrap().0.unwrap();
14108
14109        let mut frontend = FrontendState::new();
14110
14111        let ctx = ExecutorContext::new_mock(None).await;
14112        let version = Version(0);
14113
14114        frontend.program = program.clone();
14115        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14116        frontend.update_state_after_exec(outcome, true);
14117        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14118        let sketch_id = sketch_object.id;
14119        let sketch = expect_sketch(sketch_object);
14120        let line_id = *sketch.segments.get(2).unwrap();
14121
14122        let constraint = Constraint::Midpoint(Midpoint {
14123            point: ConstraintSegment::ORIGIN,
14124            segment: line_id,
14125        });
14126        let (src_delta, scene_delta) = frontend
14127            .add_constraint(&ctx, version, sketch_id, constraint)
14128            .await
14129            .unwrap();
14130        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14131        assert_eq!(
14132            scene_delta.new_graph.objects.len(),
14133            6,
14134            "{:#?}",
14135            scene_delta.new_graph.objects
14136        );
14137
14138        ctx.close().await;
14139    }
14140
14141    #[tokio::test(flavor = "multi_thread")]
14142    async fn test_origin_arc_midpoint() {
14143        let initial_source = "\
14144sketch(on = XY) {
14145  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14146}
14147";
14148
14149        let program = Program::parse(initial_source).unwrap().0.unwrap();
14150
14151        let mut frontend = FrontendState::new();
14152
14153        let ctx = ExecutorContext::new_mock(None).await;
14154        let version = Version(0);
14155
14156        frontend.program = program.clone();
14157        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14158        frontend.update_state_after_exec(outcome, true);
14159        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14160        let sketch_id = sketch_object.id;
14161        let sketch = expect_sketch(sketch_object);
14162        let arc_id = *sketch.segments.get(3).unwrap();
14163
14164        let constraint = Constraint::Midpoint(Midpoint {
14165            point: ConstraintSegment::ORIGIN,
14166            segment: arc_id,
14167        });
14168        let (src_delta, scene_delta) = frontend
14169            .add_constraint(&ctx, version, sketch_id, constraint)
14170            .await
14171            .unwrap();
14172        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14173        assert_eq!(
14174            scene_delta.new_graph.objects.len(),
14175            7,
14176            "{:#?}",
14177            scene_delta.new_graph.objects
14178        );
14179
14180        ctx.close().await;
14181    }
14182
14183    #[tokio::test(flavor = "multi_thread")]
14184    async fn test_segments_symmetric_arcs() {
14185        let initial_source = "\
14186sketch(on = XY) {
14187  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14188  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14189  line(start = [var 0, var -10], end = [var 0, var 10])
14190}
14191";
14192
14193        let program = Program::parse(initial_source).unwrap().0.unwrap();
14194
14195        let mut frontend = FrontendState::new();
14196
14197        let ctx = ExecutorContext::new_mock(None).await;
14198        let version = Version(0);
14199
14200        frontend.program = program.clone();
14201        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14202        frontend.update_state_after_exec(outcome, true);
14203        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14204        let sketch_id = sketch_object.id;
14205        let sketch = expect_sketch(sketch_object);
14206        let arc1_id = *sketch.segments.get(3).unwrap();
14207        let arc2_id = *sketch.segments.get(7).unwrap();
14208        let axis_id = *sketch.segments.get(10).unwrap();
14209
14210        let constraint = Constraint::Symmetric(Symmetric {
14211            input: vec![arc1_id, arc2_id],
14212            axis: axis_id,
14213        });
14214        let (src_delta, scene_delta) = frontend
14215            .add_constraint(&ctx, version, sketch_id, constraint)
14216            .await
14217            .unwrap();
14218        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14219        assert_eq!(
14220            scene_delta.new_graph.objects.len(),
14221            14,
14222            "{:#?}",
14223            scene_delta.new_graph.objects
14224        );
14225
14226        ctx.close().await;
14227    }
14228
14229    #[tokio::test(flavor = "multi_thread")]
14230    async fn test_sketch_on_face_simple() {
14231        let initial_source = "\
14232len = 2mm
14233cube = startSketchOn(XY)
14234  |> startProfile(at = [0, 0])
14235  |> line(end = [len, 0], tag = $side)
14236  |> line(end = [0, len])
14237  |> line(end = [-len, 0])
14238  |> line(end = [0, -len])
14239  |> close()
14240  |> extrude(length = len)
14241
14242face = faceOf(cube, face = side)
14243";
14244
14245        let program = Program::parse(initial_source).unwrap().0.unwrap();
14246
14247        let mut frontend = FrontendState::new();
14248
14249        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14250        let mock_ctx = ExecutorContext::new_mock(None).await;
14251        let version = Version(0);
14252
14253        frontend.hack_set_program(&ctx, program).await.unwrap();
14254        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14255        let face_id = face_object.id;
14256
14257        let sketch_args = SketchCtor {
14258            on: Plane::Object(face_id),
14259        };
14260        let (_src_delta, scene_delta, sketch_id) = frontend
14261            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14262            .await
14263            .unwrap();
14264        assert_eq!(sketch_id, ObjectId(2));
14265        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14266        let sketch_object = &scene_delta.new_graph.objects[2];
14267        assert_eq!(sketch_object.id, ObjectId(2));
14268        assert_eq!(
14269            sketch_object.kind,
14270            ObjectKind::Sketch(Sketch {
14271                args: SketchCtor {
14272                    on: Plane::Object(face_id),
14273                },
14274                plane: face_id,
14275                segments: vec![],
14276                constraints: vec![],
14277            })
14278        );
14279        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14280
14281        ctx.close().await;
14282        mock_ctx.close().await;
14283    }
14284
14285    #[tokio::test(flavor = "multi_thread")]
14286    async fn test_new_sketch_on_primitive_index_face() {
14287        let initial_source = "\
14288@settings(kclVersion = 2.0)
14289
14290sketch001 = sketch(on = XY) {
14291  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14292}
14293extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14294shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14295        let program = Program::parse(initial_source).unwrap().0.unwrap();
14296        let ctx = ExecutorContext::new_mock(None).await;
14297        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14298        let solid_id = match outcome.variables.get("shell001") {
14299            Some(KclValueView::Solid { value }) => value.id,
14300            value => panic!("expected shell001 to be a solid, got {value:?}"),
14301        };
14302        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14303
14304        let mut ast = program.ast;
14305        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14306        let face_expr = sketch_on_ast_expr(
14307            &mut ast,
14308            &scene_graph,
14309            &solid_references,
14310            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14311        )
14312        .unwrap();
14313        let face_decl = ast::VariableDeclaration::new(
14314            ast::VariableDeclarator::new("face001", face_expr),
14315            ast::ItemVisibility::Default,
14316            ast::VariableKind::Const,
14317        );
14318        ast.body
14319            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14320                face_decl,
14321            ))));
14322        let face_source = source_from_ast(&ast);
14323        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14324        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14325
14326        let program = Program::parse(&new_source).unwrap().0.unwrap();
14327        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14328        ctx.close().await;
14329    }
14330
14331    #[tokio::test(flavor = "multi_thread")]
14332    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14333        let initial_source = "\
14334s = sketch(on = YZ) {
14335  line1 = line(start = [0, 0], end = [0, 1])
14336  line2 = line(start = [0, 1], end = [1, 1])
14337  line3 = line(start = [1, 1], end = [0, 0])
14338}
14339region001 = region(point = [0.1, 0.1], sketch = s)
14340extrude001 = extrude(region001, length = 5)
14341";
14342
14343        let program = Program::parse(initial_source).unwrap().0.unwrap();
14344
14345        let mut frontend = FrontendState::new();
14346        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14347        let version = Version(0);
14348
14349        frontend.hack_set_program(&ctx, program).await.unwrap();
14350        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14351
14352        let sketch_args = SketchCtor {
14353            on: Plane::Object(wall_object_id),
14354        };
14355        let (src_delta, _scene_delta, _sketch_id) = frontend
14356            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14357            .await
14358            .unwrap();
14359        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14360
14361        ctx.close().await;
14362    }
14363
14364    #[tokio::test(flavor = "multi_thread")]
14365    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14366        let initial_source = "\
14367sketch001 = sketch(on = YZ) {
14368  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14369  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14370  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14371  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14372  coincident([line1.end, line2.start])
14373  coincident([line2.end, line3.start])
14374  coincident([line3.end, line4.start])
14375  coincident([line4.end, line1.start])
14376  parallel([line2, line4])
14377  parallel([line3, line1])
14378  perpendicular([line1, line2])
14379  horizontal(line3)
14380  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14381}
14382region001 = region(point = [3.1, 3.74], sketch = sketch001)
14383extrude001 = extrude(region001, length = 5)
14384";
14385
14386        let program = Program::parse(initial_source).unwrap().0.unwrap();
14387
14388        let mut frontend = FrontendState::new();
14389        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14390        let version = Version(0);
14391
14392        frontend.hack_set_program(&ctx, program).await.unwrap();
14393        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14394
14395        let sketch_args = SketchCtor {
14396            on: Plane::Object(wall_object_id),
14397        };
14398        let (src_delta, _scene_delta, _sketch_id) = frontend
14399            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14400            .await
14401            .unwrap();
14402        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14403
14404        ctx.close().await;
14405    }
14406
14407    #[tokio::test(flavor = "multi_thread")]
14408    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14409        let initial_source = "\
14410@settings(kclVersion = 2.0)
14411
14412sketch001 = sketch(on = XY) {
14413  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14414  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14415}
14416hidden001 = hide(sketch001)
14417region001 = region(segments = [sketch001.circle2])
14418region002 = region(segments = [sketch001.circle1])
14419extrude001 = extrude([region001, region002], length = 5)
14420";
14421
14422        let program = Program::parse(initial_source).unwrap().0.unwrap();
14423        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14424        let version = Version(0);
14425
14426        for (solid_output_index, expected_face) in [
14427            (0, "faceOf(extrude001[0], face = END)"),
14428            (1, "faceOf(extrude001[1], face = END)"),
14429        ] {
14430            let mut frontend = FrontendState::new();
14431            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14432            let cap_object_id = find_cap_object_id_with_solid_output_index(
14433                &frontend.scene_graph,
14434                crate::frontend::api::CapKind::End,
14435                solid_output_index,
14436            )
14437            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14438
14439            let sketch_args = SketchCtor {
14440                on: Plane::Object(cap_object_id),
14441            };
14442            let (src_delta, _scene_delta, _sketch_id) = frontend
14443                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14444                .await
14445                .unwrap();
14446
14447            assert!(
14448                src_delta.text.contains(expected_face),
14449                "expected `{expected_face}` in:\n{}",
14450                src_delta.text
14451            );
14452            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14453        }
14454
14455        ctx.close().await;
14456    }
14457
14458    #[tokio::test(flavor = "multi_thread")]
14459    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14460        let initial_source = "\
14461@settings(kclVersion = 2.0)
14462
14463sketch001 = sketch(on = XY) {
14464  rect1Line1 = line(start = [0, 0], end = [1, 0])
14465  rect1Line2 = line(start = [1, 0], end = [1, 1])
14466  rect1Line3 = line(start = [1, 1], end = [0, 1])
14467  rect1Line4 = line(start = [0, 1], end = [0, 0])
14468  rect2Line1 = line(start = [3, 0], end = [4, 0])
14469  rect2Line2 = line(start = [4, 0], end = [4, 1])
14470  rect2Line3 = line(start = [4, 1], end = [3, 1])
14471  rect2Line4 = line(start = [3, 1], end = [3, 0])
14472}
14473hidden001 = hide(sketch001)
14474region001 = region(segments = [
14475  sketch001.rect1Line4,
14476  sketch001.rect1Line1
14477])
14478region002 = region(segments = [
14479  sketch001.rect2Line4,
14480  sketch001.rect2Line1
14481])
14482extrude001 = extrude([region001, region002], length = 5)
14483";
14484
14485        let program = Program::parse(initial_source).unwrap().0.unwrap();
14486        let mut frontend = FrontendState::new();
14487        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14488        let version = Version(0);
14489
14490        frontend.hack_set_program(&ctx, program).await.unwrap();
14491        let region_call = "\
14492region(segments = [
14493  sketch001.rect1Line4,
14494  sketch001.rect1Line1
14495])";
14496        let region_call_start = initial_source.find(region_call).unwrap();
14497        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14498        let segment_call = "line(start = [0, 0], end = [1, 0])";
14499        let segment_call_start = initial_source.find(segment_call).unwrap();
14500        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14501        let wall_object_id = frontend
14502            .scene_graph
14503            .objects
14504            .iter()
14505            .find_map(|object| match &object.kind {
14506                ObjectKind::Wall(wall)
14507                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14508                        && wall.source.segment.range == segment_range =>
14509                {
14510                    Some(object.id)
14511                }
14512                _ => None,
14513            })
14514            .expect("expected a wall object for region001.tags.rect1Line1");
14515
14516        let sketch_args = SketchCtor {
14517            on: Plane::Object(wall_object_id),
14518        };
14519        let (src_delta, _scene_delta, _sketch_id) = frontend
14520            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14521            .await
14522            .unwrap();
14523
14524        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14525        assert!(
14526            src_delta.text.contains(expected_face),
14527            "expected `{expected_face}` in:\n{}",
14528            src_delta.text
14529        );
14530        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14531
14532        ctx.close().await;
14533    }
14534
14535    #[test]
14536    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14537        let source = "\
14538sketch001 = sketch(on = XY) {
14539  line(start = [0, 0], end = [1, 0])
14540}
14541part = subtract(boxSolid, tools = [cutSolid])
14542  |> appearance(color = \"#8f96a3\")
14543";
14544        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14545        let line_start = source.find("line").unwrap();
14546        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14547        let line_ref = SourceRef::Simple {
14548            range: [line_start, line_end, 0].into(),
14549            node_path: None,
14550        };
14551        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14552
14553        let subtract_start = source.find("subtract").unwrap();
14554        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14555        let subtract_ref = SourceRef::Simple {
14556            range: [subtract_start, subtract_end, 0].into(),
14557            node_path: None,
14558        };
14559        assert_eq!(
14560            variable_name_containing_source_ref(&ast, &subtract_ref),
14561            Some("part".to_owned())
14562        );
14563    }
14564
14565    #[tokio::test(flavor = "multi_thread")]
14566    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14567        clear_mem_cache().await;
14568        let source = "\
14569boxSolid = startSketchOn(XY)
14570  |> startProfile(at = [0, 0])
14571  |> line(end = [4, 0], tag = $bottomEdge)
14572  |> line(end = [0, 4])
14573  |> line(end = [-4, 0])
14574  |> close()
14575  |> extrude(length = 10)
14576cutSolid = startSketchOn(XY)
14577  |> startProfile(at = [1, 1])
14578  |> line(end = [1, 0])
14579  |> line(end = [0, 1])
14580  |> line(end = [-1, 0])
14581  |> close()
14582  |> extrude(length = 10)
14583part = subtract(boxSolid, tools = [cutSolid])
14584  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14585";
14586        let program = Program::parse(source).unwrap().0.unwrap();
14587        let mut frontend = FrontendState::new();
14588        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14589        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14590            SetProgramOutcome::Success { .. } => {}
14591            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14592        }
14593
14594        let sweep_call_start = source.find("extrude").unwrap();
14595        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14596        let part_call_start = source.find("subtract").unwrap();
14597        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14598        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14599        let composite_range = [part_call_start, part_call_end, 0].into();
14600
14601        let cap_object = frontend
14602            .scene_graph
14603            .objects
14604            .iter()
14605            .find(|object| {
14606                matches!(
14607                    &object.kind,
14608                    ObjectKind::Cap(crate::frontend::api::Cap {
14609                        kind: crate::frontend::api::CapKind::End,
14610                        source,
14611                        ..
14612                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14613                )
14614            })
14615            .expect("expected end cap object to trace through subtract and original extrude");
14616
14617        let mut ast = frontend.program.ast.clone();
14618        let cap_expr = sketch_on_ast_expr(
14619            &mut ast,
14620            &frontend.scene_graph,
14621            &frontend.solid_references,
14622            &Plane::Object(cap_object.id),
14623        )
14624        .unwrap();
14625        let cap_face_decl = ast::VariableDeclaration::new(
14626            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14627            ast::ItemVisibility::Default,
14628            ast::VariableKind::Const,
14629        );
14630        ast.body
14631            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14632                cap_face_decl,
14633            ))));
14634        let generated_source = source_from_ast(&ast);
14635
14636        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14637        assert!(!generated_source.contains("faceOf(boxSolid"));
14638        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14639            panic!("expected faceOf call");
14640        };
14641        assert_eq!(call.callee.name.name, "faceOf");
14642        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14643            panic!("expected solid name");
14644        };
14645        assert_eq!(solid_name.name.name, "part");
14646        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14647            panic!("expected face name");
14648        };
14649        assert_eq!(face_name.name.name, "END");
14650
14651        ctx.close().await;
14652    }
14653
14654    #[tokio::test(flavor = "multi_thread")]
14655    async fn test_sketch_on_plane_incremental() {
14656        let initial_source = "\
14657len = 2mm
14658cube = startSketchOn(XY)
14659  |> startProfile(at = [0, 0])
14660  |> line(end = [len, 0], tag = $side)
14661  |> line(end = [0, len])
14662  |> line(end = [-len, 0])
14663  |> line(end = [0, -len])
14664  |> close()
14665  |> extrude(length = len)
14666
14667plane = planeOf(cube, face = side)
14668";
14669
14670        let program = Program::parse(initial_source).unwrap().0.unwrap();
14671
14672        let mut frontend = FrontendState::new();
14673
14674        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14675        let mock_ctx = ExecutorContext::new_mock(None).await;
14676        let version = Version(0);
14677
14678        frontend.hack_set_program(&ctx, program).await.unwrap();
14679        // Find the last plane since the first plane is the XY plane.
14680        let plane_object = frontend
14681            .scene_graph
14682            .objects
14683            .iter()
14684            .rev()
14685            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14686            .unwrap();
14687        let plane_id = plane_object.id;
14688
14689        let sketch_args = SketchCtor {
14690            on: Plane::Object(plane_id),
14691        };
14692        let (src_delta, scene_delta, sketch_id) = frontend
14693            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14694            .await
14695            .unwrap();
14696        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14697        assert_eq!(sketch_id, ObjectId(2));
14698        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14699        let sketch_object = &scene_delta.new_graph.objects[2];
14700        assert_eq!(sketch_object.id, ObjectId(2));
14701        assert_eq!(
14702            sketch_object.kind,
14703            ObjectKind::Sketch(Sketch {
14704                args: SketchCtor {
14705                    on: Plane::Object(plane_id),
14706                },
14707                plane: plane_id,
14708                segments: vec![],
14709                constraints: vec![],
14710            })
14711        );
14712        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14713
14714        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14715        assert_eq!(plane_object.id, plane_id);
14716        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14717
14718        ctx.close().await;
14719        mock_ctx.close().await;
14720    }
14721
14722    #[tokio::test(flavor = "multi_thread")]
14723    async fn test_new_sketch_uses_unique_variable_name() {
14724        let initial_source = "\
14725sketch1 = sketch(on = XY) {
14726}
14727";
14728
14729        let program = Program::parse(initial_source).unwrap().0.unwrap();
14730
14731        let mut frontend = FrontendState::new();
14732        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14733        let version = Version(0);
14734
14735        frontend.hack_set_program(&ctx, program).await.unwrap();
14736
14737        let sketch_args = SketchCtor {
14738            on: Plane::Default(PlaneName::Yz),
14739        };
14740        let (src_delta, _, _) = frontend
14741            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14742            .await
14743            .unwrap();
14744
14745        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14746
14747        ctx.close().await;
14748    }
14749
14750    #[tokio::test(flavor = "multi_thread")]
14751    async fn test_new_sketch_twice_using_same_plane() {
14752        let initial_source = "\
14753sketch1 = sketch(on = XY) {
14754}
14755";
14756
14757        let program = Program::parse(initial_source).unwrap().0.unwrap();
14758
14759        let mut frontend = FrontendState::new();
14760        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14761        let version = Version(0);
14762
14763        frontend.hack_set_program(&ctx, program).await.unwrap();
14764
14765        let sketch_args = SketchCtor {
14766            on: Plane::Default(PlaneName::Xy),
14767        };
14768        let (src_delta, _, _) = frontend
14769            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14770            .await
14771            .unwrap();
14772
14773        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14774
14775        ctx.close().await;
14776    }
14777
14778    #[tokio::test(flavor = "multi_thread")]
14779    async fn test_sketch_mode_reuses_cached_on_expression() {
14780        let initial_source = "\
14781width = 2mm
14782sketch(on = offsetPlane(XY, offset = width)) {
14783  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14784  distance([line1.start, line1.end]) == width
14785}
14786";
14787        let program = Program::parse(initial_source).unwrap().0.unwrap();
14788
14789        let mut frontend = FrontendState::new();
14790        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14791        let mock_ctx = ExecutorContext::new_mock(None).await;
14792        let version = Version(0);
14793        let project_id = ProjectId(0);
14794        let file_id = FileId(0);
14795
14796        frontend.hack_set_program(&ctx, program).await.unwrap();
14797        let initial_object_count = frontend.scene_graph.objects.len();
14798        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14799            .expect("Expected sketch object to exist")
14800            .id;
14801
14802        // Entering sketch mode should reuse cached `on` expression state
14803        // (offsetPlane result), not fail or create extra on-surface objects.
14804        let scene_delta = frontend
14805            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14806            .await
14807            .unwrap();
14808        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14809
14810        // A follow-up sketch-mode execution should keep the same stable object
14811        // graph shape as well.
14812        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14813        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14814
14815        ctx.close().await;
14816        mock_ctx.close().await;
14817    }
14818
14819    #[tokio::test(flavor = "multi_thread")]
14820    async fn test_edit_sketch_nested_in_pipe() {
14821        clear_mem_cache().await;
14822        let source = r#"
14823profile = sketch(on = XY) {
14824  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14825}
14826  |> translate(x = 2mm)
14827"#;
14828        let program = Program::parse_no_errs(source).unwrap();
14829        let mut frontend = FrontendState::new();
14830        let mock_ctx = ExecutorContext::new_mock(None).await;
14831        let version = Version(0);
14832
14833        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14834        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14835            .expect("Expected piped sketch object")
14836            .id;
14837
14838        let scene_delta = frontend
14839            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14840            .await
14841            .unwrap();
14842        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14843        assert!(
14844            scene_delta
14845                .new_graph
14846                .objects
14847                .iter()
14848                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14849            "Expected the piped sketch's segments to be present in sketch mode"
14850        );
14851
14852        clear_mem_cache().await;
14853        mock_ctx.close().await;
14854    }
14855
14856    #[tokio::test(flavor = "multi_thread")]
14857    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14858        clear_mem_cache().await;
14859        let source = r#"
14860useFirstProfile = true
14861
14862profile = if useFirstProfile {
14863  sketch(on = XY) {
14864    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14865  }
14866} else {
14867  sketch(on = XY) {
14868    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14869  }
14870}
14871"#;
14872        let program = Program::parse_no_errs(source).unwrap();
14873        let mut frontend = FrontendState::new();
14874        let mock_ctx = ExecutorContext::new_mock(None).await;
14875        let version = Version(0);
14876
14877        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14878        let sketch_object =
14879            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14880        let sketch_id = sketch_object.id;
14881        let sketch = expect_sketch(sketch_object);
14882        let line_end_id = *sketch
14883            .segments
14884            .get(1)
14885            .expect("Expected the active branch's line end point");
14886
14887        let scene_delta = frontend
14888            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14889            .await
14890            .unwrap();
14891        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14892
14893        let segments = vec![ExistingSegmentCtor {
14894            id: line_end_id,
14895            ctor: SegmentCtor::Point(PointCtor {
14896                position: Point2d {
14897                    x: Expr::Var(Number {
14898                        value: 30.0,
14899                        units: NumericSuffix::Mm,
14900                    }),
14901                    y: Expr::Var(Number {
14902                        value: 15.0,
14903                        units: NumericSuffix::Mm,
14904                    }),
14905                },
14906            }),
14907        }];
14908        let (source_delta, _) = frontend
14909            .edit_segments(&mock_ctx, version, sketch_id, segments)
14910            .await
14911            .unwrap();
14912        assert!(
14913            source_delta
14914                .text
14915                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14916            "Expected the active branch's dragged variables to be updated:\n{}",
14917            source_delta.text
14918        );
14919        assert!(
14920            source_delta
14921                .text
14922                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14923            "Expected the inactive branch to remain unchanged:\n{}",
14924            source_delta.text
14925        );
14926
14927        clear_mem_cache().await;
14928        mock_ctx.close().await;
14929    }
14930
14931    #[tokio::test(flavor = "multi_thread")]
14932    async fn test_multiple_sketch_blocks() {
14933        let initial_source = "\
14934// Cube that requires the engine.
14935width = 2
14936sketch001 = startSketchOn(XY)
14937profile001 = startProfile(sketch001, at = [0, 0])
14938  |> yLine(length = width, tag = $seg1)
14939  |> xLine(length = width)
14940  |> yLine(length = -width)
14941  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14942  |> close()
14943extrude001 = extrude(profile001, length = width)
14944
14945// Get a value that requires the engine.
14946x = segLen(seg1)
14947
14948// Triangle with side length 2*x.
14949sketch(on = XY) {
14950  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14951  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14952  coincident([line1.end, line2.start])
14953  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14954  coincident([line2.end, line3.start])
14955  coincident([line3.end, line1.start])
14956  equalLength([line3, line1])
14957  equalLength([line1, line2])
14958  distance([line1.start, line1.end]) == 2*x
14959}
14960
14961// Line segment with length x.
14962sketch2 = sketch(on = XY) {
14963  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14964  distance([line1.start, line1.end]) == x
14965}
14966";
14967
14968        let program = Program::parse(initial_source).unwrap().0.unwrap();
14969
14970        let mut frontend = FrontendState::new();
14971
14972        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14973        let mock_ctx = ExecutorContext::new_mock(None).await;
14974        let version = Version(0);
14975        let project_id = ProjectId(0);
14976        let file_id = FileId(0);
14977
14978        frontend.hack_set_program(&ctx, program).await.unwrap();
14979        let sketch_objects = frontend
14980            .scene_graph
14981            .objects
14982            .iter()
14983            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14984            .collect::<Vec<_>>();
14985        let sketch1_id = sketch_objects.first().unwrap().id;
14986        let sketch2_id = sketch_objects.get(1).unwrap().id;
14987        // First point in sketch1.
14988        let point1_id = ObjectId(sketch1_id.0 + 1);
14989        // First point in sketch2.
14990        let point2_id = ObjectId(sketch2_id.0 + 1);
14991
14992        // Edit the first sketch. Objects before the sketch block should be
14993        // present from execution cache so that we can sketch on prior planes,
14994        // for example. Objects after the first sketch block should not be
14995        // present since those statements are skipped in sketch mode.
14996        //
14997        // - startSketchOn(XY) Plane 1
14998        // - sketch on=XY Plane 1
14999        // - Sketch block 16
15000        let scene_delta = frontend
15001            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15002            .await
15003            .unwrap();
15004        assert_eq!(
15005            scene_delta.new_graph.objects.len(),
15006            18,
15007            "{:#?}",
15008            scene_delta.new_graph.objects
15009        );
15010
15011        // Edit a point in the first sketch.
15012        let point_ctor = PointCtor {
15013            position: Point2d {
15014                x: Expr::Var(Number {
15015                    value: 1.0,
15016                    units: NumericSuffix::Mm,
15017                }),
15018                y: Expr::Var(Number {
15019                    value: 2.0,
15020                    units: NumericSuffix::Mm,
15021                }),
15022            },
15023        };
15024        let segments = vec![ExistingSegmentCtor {
15025            id: point1_id,
15026            ctor: SegmentCtor::Point(point_ctor),
15027        }];
15028        let (src_delta, _) = frontend
15029            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15030            .await
15031            .unwrap();
15032        // Only the first sketch block changes.
15033        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15034        let edited_sketch1_source = src_delta.text.clone();
15035
15036        // Execute mock to simulate drag end.
15037        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15038        assert_eq!(src_delta.text, edited_sketch1_source);
15039        // Exit sketch. Objects from the entire program should be present.
15040        //
15041        // - startSketchOn(XY) Plane 1
15042        // - sketch on=XY Plane 1
15043        // - Sketch block 16
15044        // - sketch on=XY cached
15045        // - Sketch block 5
15046        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15047        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15048
15049        // Edit the second sketch.
15050        //
15051        // - startSketchOn(XY) Plane 1
15052        // - sketch on=XY Plane 1
15053        // - Sketch block 16
15054        // - sketch on=XY cached
15055        // - Sketch block 5
15056        let scene_delta = frontend
15057            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15058            .await
15059            .unwrap();
15060        assert_eq!(
15061            scene_delta.new_graph.objects.len(),
15062            24,
15063            "{:#?}",
15064            scene_delta.new_graph.objects
15065        );
15066
15067        // Edit a point in the second sketch.
15068        let point_ctor = PointCtor {
15069            position: Point2d {
15070                x: Expr::Var(Number {
15071                    value: 3.0,
15072                    units: NumericSuffix::Mm,
15073                }),
15074                y: Expr::Var(Number {
15075                    value: 4.0,
15076                    units: NumericSuffix::Mm,
15077                }),
15078            },
15079        };
15080        let segments = vec![ExistingSegmentCtor {
15081            id: point2_id,
15082            ctor: SegmentCtor::Point(point_ctor),
15083        }];
15084        let (src_delta, _) = frontend
15085            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15086            .await
15087            .unwrap();
15088        // Only the second sketch block changes.
15089        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15090        let edited_sketch2_source = src_delta.text.clone();
15091
15092        // Execute mock to simulate drag end.
15093        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15094        assert_eq!(src_delta.text, edited_sketch2_source);
15095
15096        ctx.close().await;
15097        mock_ctx.close().await;
15098    }
15099
15100    #[tokio::test(flavor = "multi_thread")]
15101    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15102        clear_mem_cache().await;
15103
15104        let source = r#"sketch001 = sketch(on = XZ) {
15105  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15106}
15107sketch002 = sketch(on = XY) {
15108  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15109  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15110  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15111  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15112  coincident([line1.end, line2.start])
15113  coincident([line2.end, line3.start])
15114  coincident([line3.end, line4.start])
15115  coincident([line4.end, line1.start])
15116  parallel([line2, line4])
15117  parallel([line3, line1])
15118  perpendicular([line1, line2])
15119  horizontal(line3)
15120  coincident([line1.start, ORIGIN])
15121}
15122"#;
15123
15124        let program = Program::parse(source).unwrap().0.unwrap();
15125        let mut frontend = FrontendState::new();
15126        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15127        let mock_ctx = ExecutorContext::new_mock(None).await;
15128        let version = Version(0);
15129        let project_id = ProjectId(0);
15130        let file_id = FileId(0);
15131
15132        frontend.hack_set_program(&ctx, program).await.unwrap();
15133        let sketch_objects = frontend
15134            .scene_graph
15135            .objects
15136            .iter()
15137            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15138            .collect::<Vec<_>>();
15139        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15140
15141        let sketch1_id = sketch_objects[0].id;
15142        let sketch2_id = sketch_objects[1].id;
15143
15144        frontend
15145            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15146            .await
15147            .unwrap();
15148        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15149
15150        let scene_delta = frontend
15151            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15152            .await
15153            .unwrap();
15154        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15155
15156        clear_mem_cache().await;
15157        ctx.close().await;
15158        mock_ctx.close().await;
15159    }
15160
15161    // Regression tests: operations on source code with extra whitespace/newlines.
15162    // These test that NodePath-based lookups work correctly when source ranges
15163    // are shifted by extra whitespace that wouldn't be present after formatting.
15164
15165    #[tokio::test(flavor = "multi_thread")]
15166    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15167        // Extra newlines after @settings line - this shifts all source ranges.
15168        let initial_source = "@settings(defaultLengthUnit = mm)
15169
15170sketch001 = sketch(on = XY) {
15171  point(at = [1in, 2in])
15172}
15173";
15174
15175        let program = Program::parse(initial_source).unwrap().0.unwrap();
15176        let mut frontend = FrontendState::new();
15177
15178        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15179        let mock_ctx = ExecutorContext::new_mock(None).await;
15180        let version = Version(0);
15181        let project_id = ProjectId(0);
15182        let file_id = FileId(0);
15183
15184        frontend.hack_set_program(&ctx, program).await.unwrap();
15185        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15186        let sketch_id = sketch_object.id;
15187
15188        // Edit sketch should succeed despite extra newlines.
15189        frontend
15190            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15191            .await
15192            .unwrap();
15193
15194        // Add a new point to the sketch.
15195        let point_ctor = PointCtor {
15196            position: Point2d {
15197                x: Expr::Number(Number {
15198                    value: 5.0,
15199                    units: NumericSuffix::Mm,
15200                }),
15201                y: Expr::Number(Number {
15202                    value: 6.0,
15203                    units: NumericSuffix::Mm,
15204                }),
15205            },
15206        };
15207        let segment = SegmentCtor::Point(point_ctor);
15208        let (src_delta, scene_delta) = frontend
15209            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15210            .await
15211            .unwrap();
15212        // After adding a point, the source should be reformatted with standard whitespace.
15213        assert!(
15214            src_delta.text.contains("point(at = [5mm, 6mm])"),
15215            "Expected new point in source, got: {}",
15216            src_delta.text
15217        );
15218        assert!(!scene_delta.new_objects.is_empty());
15219
15220        ctx.close().await;
15221        mock_ctx.close().await;
15222    }
15223
15224    #[tokio::test(flavor = "multi_thread")]
15225    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15226        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15227
15228        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15229        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15230
15231        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15232        assert!(
15233            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15234            "Expected experimental settings to be added to source"
15235        );
15236    }
15237
15238    #[tokio::test(flavor = "multi_thread")]
15239    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15240        // Extra newlines after @settings, with an empty sketch block.
15241        let initial_source = "@settings(defaultLengthUnit = mm)
15242
15243s = sketch(on = XY) {}
15244";
15245
15246        let program = Program::parse(initial_source).unwrap().0.unwrap();
15247        let mut frontend = FrontendState::new();
15248
15249        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15250        let mock_ctx = ExecutorContext::new_mock(None).await;
15251        let version = Version(0);
15252
15253        frontend.hack_set_program(&ctx, program).await.unwrap();
15254        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15255        let sketch_id = sketch_object.id;
15256
15257        let line_ctor = LineCtor {
15258            start: Point2d {
15259                x: Expr::Number(Number {
15260                    value: 0.0,
15261                    units: NumericSuffix::Mm,
15262                }),
15263                y: Expr::Number(Number {
15264                    value: 0.0,
15265                    units: NumericSuffix::Mm,
15266                }),
15267            },
15268            end: Point2d {
15269                x: Expr::Number(Number {
15270                    value: 10.0,
15271                    units: NumericSuffix::Mm,
15272                }),
15273                y: Expr::Number(Number {
15274                    value: 10.0,
15275                    units: NumericSuffix::Mm,
15276                }),
15277            },
15278            construction: None,
15279        };
15280        let segment = SegmentCtor::Line(line_ctor);
15281        let (src_delta, scene_delta) = frontend
15282            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15283            .await
15284            .unwrap();
15285        assert!(
15286            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15287            "Expected line in source, got: {}",
15288            src_delta.text
15289        );
15290        // Line creates start point, end point, and line segment.
15291        assert_eq!(scene_delta.new_objects.len(), 3);
15292
15293        ctx.close().await;
15294        mock_ctx.close().await;
15295    }
15296
15297    #[tokio::test(flavor = "multi_thread")]
15298    async fn test_extra_newlines_between_operations_edit_line() {
15299        // Extra newlines between @settings and sketch, and inside the sketch block.
15300        let initial_source = "@settings(defaultLengthUnit = mm)
15301
15302sketch001 = sketch(on = XY) {
15303
15304  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15305
15306}
15307";
15308
15309        let program = Program::parse(initial_source).unwrap().0.unwrap();
15310        let mut frontend = FrontendState::new();
15311
15312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15313        let mock_ctx = ExecutorContext::new_mock(None).await;
15314        let version = Version(0);
15315        let project_id = ProjectId(0);
15316        let file_id = FileId(0);
15317
15318        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15319        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15320        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15321        let sketch_id = sketch_object.id;
15322        let sketch = expect_sketch(sketch_object);
15323
15324        // Extract segment IDs before edit_sketch borrows frontend mutably.
15325        let line_id = sketch
15326            .segments
15327            .iter()
15328            .copied()
15329            .find(|seg_id| {
15330                matches!(
15331                    &frontend.scene_graph.objects[seg_id.0].kind,
15332                    ObjectKind::Segment {
15333                        segment: Segment::Line(_)
15334                    }
15335                )
15336            })
15337            .expect("Expected a line segment in sketch");
15338
15339        // Enter sketch edit mode.
15340        frontend
15341            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15342            .await
15343            .unwrap();
15344
15345        // Edit the line.
15346        let line_ctor = LineCtor {
15347            start: Point2d {
15348                x: Expr::Var(Number {
15349                    value: 1.0,
15350                    units: NumericSuffix::Mm,
15351                }),
15352                y: Expr::Var(Number {
15353                    value: 2.0,
15354                    units: NumericSuffix::Mm,
15355                }),
15356            },
15357            end: Point2d {
15358                x: Expr::Var(Number {
15359                    value: 13.0,
15360                    units: NumericSuffix::Mm,
15361                }),
15362                y: Expr::Var(Number {
15363                    value: 14.0,
15364                    units: NumericSuffix::Mm,
15365                }),
15366            },
15367            construction: None,
15368        };
15369        let segments = vec![ExistingSegmentCtor {
15370            id: line_id,
15371            ctor: SegmentCtor::Line(line_ctor),
15372        }];
15373        let (src_delta, _scene_delta) = frontend
15374            .edit_segments(&mock_ctx, version, sketch_id, segments)
15375            .await
15376            .unwrap();
15377        assert!(
15378            src_delta
15379                .text
15380                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15381            "Expected edited line in source, got: {}",
15382            src_delta.text
15383        );
15384
15385        ctx.close().await;
15386        mock_ctx.close().await;
15387    }
15388
15389    #[tokio::test(flavor = "multi_thread")]
15390    async fn test_extra_newlines_delete_segment() {
15391        // Extra whitespace before and after the sketch block.
15392        let initial_source = "@settings(defaultLengthUnit = mm)
15393
15394sketch001 = sketch(on = XY) {
15395  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15396}
15397";
15398
15399        let program = Program::parse(initial_source).unwrap().0.unwrap();
15400        let mut frontend = FrontendState::new();
15401
15402        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15403        let mock_ctx = ExecutorContext::new_mock(None).await;
15404        let version = Version(0);
15405
15406        frontend.hack_set_program(&ctx, program).await.unwrap();
15407        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15408        let sketch_id = sketch_object.id;
15409        let sketch = expect_sketch(sketch_object);
15410
15411        // The sketch should have 3 segments: start point, center point, and the circle.
15412        assert_eq!(sketch.segments.len(), 3);
15413        let circle_id = sketch.segments[2];
15414
15415        // Delete the circle despite extra newlines in original source.
15416        let (src_delta, scene_delta) = frontend
15417            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15418            .await
15419            .unwrap();
15420        assert!(
15421            src_delta.text.contains("sketch(on = XY) {"),
15422            "Expected sketch block in source, got: {}",
15423            src_delta.text
15424        );
15425        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15426        let new_sketch = expect_sketch(new_sketch_object);
15427        assert_eq!(new_sketch.segments.len(), 0);
15428
15429        ctx.close().await;
15430        mock_ctx.close().await;
15431    }
15432
15433    #[tokio::test(flavor = "multi_thread")]
15434    async fn test_unformatted_source_add_arc() {
15435        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15436        let initial_source = "@settings(defaultLengthUnit = mm)
15437
15438sketch001 = sketch(on = XY) {
15439}
15440";
15441
15442        let program = Program::parse(initial_source).unwrap().0.unwrap();
15443        let mut frontend = FrontendState::new();
15444
15445        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15446        let mock_ctx = ExecutorContext::new_mock(None).await;
15447        let version = Version(0);
15448
15449        frontend.hack_set_program(&ctx, program).await.unwrap();
15450        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15451        let sketch_id = sketch_object.id;
15452
15453        let arc_ctor = ArcCtor {
15454            start: Point2d {
15455                x: Expr::Var(Number {
15456                    value: 5.0,
15457                    units: NumericSuffix::Mm,
15458                }),
15459                y: Expr::Var(Number {
15460                    value: 0.0,
15461                    units: NumericSuffix::Mm,
15462                }),
15463            },
15464            end: Point2d {
15465                x: Expr::Var(Number {
15466                    value: 0.0,
15467                    units: NumericSuffix::Mm,
15468                }),
15469                y: Expr::Var(Number {
15470                    value: 5.0,
15471                    units: NumericSuffix::Mm,
15472                }),
15473            },
15474            center: Point2d {
15475                x: Expr::Var(Number {
15476                    value: 0.0,
15477                    units: NumericSuffix::Mm,
15478                }),
15479                y: Expr::Var(Number {
15480                    value: 0.0,
15481                    units: NumericSuffix::Mm,
15482                }),
15483            },
15484            direction: None,
15485            construction: None,
15486        };
15487        let segment = SegmentCtor::Arc(arc_ctor);
15488        let (src_delta, scene_delta) = frontend
15489            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15490            .await
15491            .unwrap();
15492        assert!(
15493            src_delta
15494                .text
15495                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15496            "Expected arc in source, got: {}",
15497            src_delta.text
15498        );
15499        assert!(!scene_delta.new_objects.is_empty());
15500
15501        ctx.close().await;
15502        mock_ctx.close().await;
15503    }
15504
15505    #[tokio::test(flavor = "multi_thread")]
15506    async fn test_arc_direction_flows_to_source() {
15507        let initial_source = "@settings(defaultLengthUnit = mm)
15508
15509sketch001 = sketch(on = XY) {
15510}
15511";
15512
15513        let program = Program::parse(initial_source).unwrap().0.unwrap();
15514        let mut frontend = FrontendState::new();
15515
15516        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15517        let mock_ctx = ExecutorContext::new_mock(None).await;
15518        let version = Version(0);
15519
15520        frontend.hack_set_program(&ctx, program).await.unwrap();
15521        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15522        let sketch_id = sketch_object.id;
15523
15524        let point = |x: f64, y: f64| Point2d {
15525            x: Expr::Var(Number {
15526                value: x,
15527                units: NumericSuffix::Mm,
15528            }),
15529            y: Expr::Var(Number {
15530                value: y,
15531                units: NumericSuffix::Mm,
15532            }),
15533        };
15534
15535        // Adding a clockwise arc writes its direction to the source.
15536        let arc_ctor = ArcCtor {
15537            start: point(5.0, 0.0),
15538            end: point(0.0, 5.0),
15539            center: point(0.0, 0.0),
15540            direction: Some(ArcDirection::Cw),
15541            construction: None,
15542        };
15543        let (src_delta, scene_delta) = frontend
15544            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15545            .await
15546            .unwrap();
15547        assert!(
15548            src_delta.text.contains("direction = CW"),
15549            "Expected direction = CW in source, got: {}",
15550            src_delta.text
15551        );
15552        // The new objects are the end points, the center, and then the arc.
15553        let arc_id = *scene_delta.new_objects.last().unwrap();
15554
15555        // Editing the arc's points preserves the clockwise direction. The
15556        // edited points keep the same distance to the center so that the
15557        // solver doesn't need to move anything.
15558        let edited_ctor = ArcCtor {
15559            start: point(0.0, -5.0),
15560            end: point(0.0, 5.0),
15561            center: point(0.0, 0.0),
15562            direction: Some(ArcDirection::Cw),
15563            construction: None,
15564        };
15565        let (src_delta, _scene_delta) = frontend
15566            .edit_segments(
15567                &mock_ctx,
15568                version,
15569                sketch_id,
15570                vec![ExistingSegmentCtor {
15571                    id: arc_id,
15572                    ctor: SegmentCtor::Arc(edited_ctor),
15573                }],
15574            )
15575            .await
15576            .unwrap();
15577        assert!(
15578            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15579            "Expected edited start point in source, got: {}",
15580            src_delta.text
15581        );
15582        assert!(
15583            src_delta.text.contains("direction = CW"),
15584            "Expected direction = CW to be preserved in source, got: {}",
15585            src_delta.text
15586        );
15587
15588        // Editing the arc back to counterclockwise removes the direction
15589        // argument since counterclockwise is the default.
15590        let edited_ctor = ArcCtor {
15591            start: point(0.0, -5.0),
15592            end: point(0.0, 5.0),
15593            center: point(0.0, 0.0),
15594            direction: Some(ArcDirection::Ccw),
15595            construction: None,
15596        };
15597        let (src_delta, _scene_delta) = frontend
15598            .edit_segments(
15599                &mock_ctx,
15600                version,
15601                sketch_id,
15602                vec![ExistingSegmentCtor {
15603                    id: arc_id,
15604                    ctor: SegmentCtor::Arc(edited_ctor),
15605                }],
15606            )
15607            .await
15608            .unwrap();
15609        assert!(
15610            !src_delta.text.contains("direction"),
15611            "Expected direction argument to be removed from source, got: {}",
15612            src_delta.text
15613        );
15614
15615        ctx.close().await;
15616        mock_ctx.close().await;
15617    }
15618
15619    #[tokio::test(flavor = "multi_thread")]
15620    async fn test_extra_newlines_add_circle() {
15621        // Extra blank lines between settings and sketch.
15622        let initial_source = "@settings(defaultLengthUnit = mm)
15623
15624sketch001 = sketch(on = XY) {
15625}
15626";
15627
15628        let program = Program::parse(initial_source).unwrap().0.unwrap();
15629        let mut frontend = FrontendState::new();
15630
15631        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15632        let mock_ctx = ExecutorContext::new_mock(None).await;
15633        let version = Version(0);
15634
15635        frontend.hack_set_program(&ctx, program).await.unwrap();
15636        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15637        let sketch_id = sketch_object.id;
15638
15639        let circle_ctor = CircleCtor {
15640            start: Point2d {
15641                x: Expr::Var(Number {
15642                    value: 5.0,
15643                    units: NumericSuffix::Mm,
15644                }),
15645                y: Expr::Var(Number {
15646                    value: 0.0,
15647                    units: NumericSuffix::Mm,
15648                }),
15649            },
15650            center: Point2d {
15651                x: Expr::Var(Number {
15652                    value: 0.0,
15653                    units: NumericSuffix::Mm,
15654                }),
15655                y: Expr::Var(Number {
15656                    value: 0.0,
15657                    units: NumericSuffix::Mm,
15658                }),
15659            },
15660            construction: None,
15661        };
15662        let segment = SegmentCtor::Circle(circle_ctor);
15663        let (src_delta, scene_delta) = frontend
15664            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15665            .await
15666            .unwrap();
15667        assert!(
15668            src_delta
15669                .text
15670                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15671            "Expected circle in source, got: {}",
15672            src_delta.text
15673        );
15674        assert!(!scene_delta.new_objects.is_empty());
15675
15676        ctx.close().await;
15677        mock_ctx.close().await;
15678    }
15679
15680    #[tokio::test(flavor = "multi_thread")]
15681    async fn test_extra_newlines_add_constraint() {
15682        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15683        let initial_source = "@settings(defaultLengthUnit = mm)
15684
15685sketch001 = sketch(on = XY) {
15686  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15687  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15688}
15689";
15690
15691        let program = Program::parse(initial_source).unwrap().0.unwrap();
15692        let mut frontend = FrontendState::new();
15693
15694        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15695        let mock_ctx = ExecutorContext::new_mock(None).await;
15696        let version = Version(0);
15697        let project_id = ProjectId(0);
15698        let file_id = FileId(0);
15699
15700        frontend.hack_set_program(&ctx, program).await.unwrap();
15701        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15702        let sketch_id = sketch_object.id;
15703        let sketch = expect_sketch(sketch_object);
15704
15705        // Extract segment data before edit_sketch borrows frontend mutably.
15706        let line_ids: Vec<ObjectId> = sketch
15707            .segments
15708            .iter()
15709            .copied()
15710            .filter(|seg_id| {
15711                matches!(
15712                    &frontend.scene_graph.objects[seg_id.0].kind,
15713                    ObjectKind::Segment {
15714                        segment: Segment::Line(_)
15715                    }
15716                )
15717            })
15718            .collect();
15719        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15720
15721        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15722        let ObjectKind::Segment {
15723            segment: Segment::Line(line1_data),
15724        } = &line1.kind
15725        else {
15726            panic!("Expected line");
15727        };
15728        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15729        let ObjectKind::Segment {
15730            segment: Segment::Line(line2_data),
15731        } = &line2.kind
15732        else {
15733            panic!("Expected line");
15734        };
15735
15736        // Build constraint before entering sketch mode.
15737        let constraint = Constraint::Coincident(Coincident {
15738            segments: vec![line1_data.end.into(), line2_data.start.into()],
15739        });
15740
15741        // Enter sketch edit mode.
15742        frontend
15743            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15744            .await
15745            .unwrap();
15746        let (src_delta, _scene_delta) = frontend
15747            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15748            .await
15749            .unwrap();
15750        assert!(
15751            src_delta.text.contains("coincident("),
15752            "Expected coincident constraint in source, got: {}",
15753            src_delta.text
15754        );
15755
15756        ctx.close().await;
15757        mock_ctx.close().await;
15758    }
15759
15760    #[tokio::test(flavor = "multi_thread")]
15761    async fn test_extra_newlines_add_line_then_edit_line() {
15762        // Extra newlines after @settings - add a line, then edit it.
15763        let initial_source = "@settings(defaultLengthUnit = mm)
15764
15765sketch001 = sketch(on = XY) {
15766}
15767";
15768
15769        let program = Program::parse(initial_source).unwrap().0.unwrap();
15770        let mut frontend = FrontendState::new();
15771
15772        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15773        let mock_ctx = ExecutorContext::new_mock(None).await;
15774        let version = Version(0);
15775
15776        frontend.hack_set_program(&ctx, program).await.unwrap();
15777        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15778        let sketch_id = sketch_object.id;
15779
15780        // Add a line.
15781        let line_ctor = LineCtor {
15782            start: Point2d {
15783                x: Expr::Number(Number {
15784                    value: 0.0,
15785                    units: NumericSuffix::Mm,
15786                }),
15787                y: Expr::Number(Number {
15788                    value: 0.0,
15789                    units: NumericSuffix::Mm,
15790                }),
15791            },
15792            end: Point2d {
15793                x: Expr::Number(Number {
15794                    value: 10.0,
15795                    units: NumericSuffix::Mm,
15796                }),
15797                y: Expr::Number(Number {
15798                    value: 10.0,
15799                    units: NumericSuffix::Mm,
15800                }),
15801            },
15802            construction: None,
15803        };
15804        let segment = SegmentCtor::Line(line_ctor);
15805        let (src_delta, scene_delta) = frontend
15806            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15807            .await
15808            .unwrap();
15809        assert!(
15810            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15811            "Expected line in source after add, got: {}",
15812            src_delta.text
15813        );
15814        // Line creates start point, end point, and line segment.
15815        let line_id = *scene_delta.new_objects.last().unwrap();
15816
15817        // Edit the line.
15818        let line_ctor = LineCtor {
15819            start: Point2d {
15820                x: Expr::Number(Number {
15821                    value: 1.0,
15822                    units: NumericSuffix::Mm,
15823                }),
15824                y: Expr::Number(Number {
15825                    value: 2.0,
15826                    units: NumericSuffix::Mm,
15827                }),
15828            },
15829            end: Point2d {
15830                x: Expr::Number(Number {
15831                    value: 13.0,
15832                    units: NumericSuffix::Mm,
15833                }),
15834                y: Expr::Number(Number {
15835                    value: 14.0,
15836                    units: NumericSuffix::Mm,
15837                }),
15838            },
15839            construction: None,
15840        };
15841        let segments = vec![ExistingSegmentCtor {
15842            id: line_id,
15843            ctor: SegmentCtor::Line(line_ctor),
15844        }];
15845        let (src_delta, scene_delta) = frontend
15846            .edit_segments(&mock_ctx, version, sketch_id, segments)
15847            .await
15848            .unwrap();
15849        assert!(
15850            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15851            "Expected edited line in source, got: {}",
15852            src_delta.text
15853        );
15854        assert_eq!(scene_delta.new_objects, vec![]);
15855
15856        ctx.close().await;
15857        mock_ctx.close().await;
15858    }
15859
15860    #[test]
15861    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15862        // A non-target sketch block appears before the target so that the
15863        // traversal enters and leaves it before reaching the target. The
15864        // generated name must come from the top-level scope, where foo1 is
15865        // taken, not the sketch's scope, where no foo names are taken. This
15866        // is a regression test: dfs_mut used to visit the sketch block twice,
15867        // pushing its scope twice but popping it once, leaving the sketch
15868        // scope on top of the defined-names stack for the rest of the
15869        // traversal.
15870        let code = "\
15871foo1 = 1
15872sk = sketch() {
15873  p = var 1.5
15874}
158757 + 8
15876";
15877        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15878        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15879            panic!("expected an expression statement");
15880        };
15881        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15882        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15883            &mut ast,
15884            &source_ref,
15885            AstMutateCommand::AddVariableDeclaration {
15886                prefix: "foo".to_owned(),
15887            },
15888        )
15889        .unwrap();
15890        let AstMutateCommandReturn::Name(name) = cmd_return else {
15891            panic!("expected a generated name");
15892        };
15893        assert_eq!(name, "foo2");
15894        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15895            panic!("expected the expression statement to become a variable declaration");
15896        };
15897        assert_eq!(decl.name(), "foo2");
15898    }
15899
15900    /// Get the function body of the variable declaration at `ast.body[index]`.
15901    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15902        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15903            panic!("expected a variable declaration");
15904        };
15905        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15906            panic!("expected a function expression");
15907        };
15908        &func.body
15909    }
15910
15911    /// Get the then-branch block of the if-expression initializing the
15912    /// variable declaration at `ast.body[index]`.
15913    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15914        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15915            panic!("expected a variable declaration");
15916        };
15917        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15918            panic!("expected an if expression");
15919        };
15920        &if_expr.then_val
15921    }
15922
15923    #[test]
15924    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15925        // The generated name must come from the function body's scope, where
15926        // thing1 is taken. Before dfs_mut visited function bodies as program
15927        // nodes, the top-level scope was used instead, generating thing1 and
15928        // colliding with the local.
15929        let code = "\
15930fn build() {
15931  thing1 = 1
15932  10 + 20
15933  return thing1
15934}
15935";
15936        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15937        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15938            panic!("expected an expression statement");
15939        };
15940        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15941        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15942            &mut ast,
15943            &source_ref,
15944            AstMutateCommand::AddVariableDeclaration {
15945                prefix: "thing".to_owned(),
15946            },
15947        )
15948        .unwrap();
15949        let AstMutateCommandReturn::Name(name) = cmd_return else {
15950            panic!("expected a generated name");
15951        };
15952        assert_eq!(name, "thing2");
15953        let body = &function_body_at(&ast, 0).body;
15954        assert_eq!(body.len(), 3);
15955        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15956            panic!("expected the expression statement to become a variable declaration");
15957        };
15958        assert_eq!(decl.name(), "thing2");
15959        // Siblings are untouched.
15960        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15961            panic!("expected a variable declaration");
15962        };
15963        assert_eq!(first.name(), "thing1");
15964        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15965    }
15966
15967    #[test]
15968    fn test_delete_node_in_function_body_preserves_leading_comment() {
15969        // Before the shared body traversal, MutateBodyItem::Delete was
15970        // silently dropped inside function bodies, so this reported success
15971        // without deleting anything.
15972        let code = "\
15973fn build() {
15974  a = 1
15975  // keep me
15976  b = 2
15977  return a
15978}
15979";
15980        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15981        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15982            panic!("expected a variable declaration");
15983        };
15984        assert_eq!(b_decl.name(), "b");
15985        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15986        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15987        let body = &function_body_at(&ast, 0).body;
15988        assert_eq!(body.len(), 2, "expected b to be deleted");
15989        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15990            panic!("expected a variable declaration");
15991        };
15992        assert_eq!(first.name(), "a");
15993        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15994            panic!("expected the return statement to remain");
15995        };
15996        assert!(
15997            body[1].get_comments().iter().any(|c| c.contains("keep me")),
15998            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15999            body[1].get_comments()
16000        );
16001    }
16002
16003    #[test]
16004    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16005        // Locals in the function body are avoided: thing1 is taken, so the
16006        // generated name is thing2. But find_defined_names only sees the
16007        // block's body items, not the function's parameters, so the generated
16008        // name collides with the thing2 parameter. This pins the current
16009        // behavior.
16010        // TODO: Should function parameters be included in the scope used for
16011        // name generation?
16012        let code = "\
16013fn build(thing2) {
16014  thing1 = 1
16015  10 + 20
16016  return thing1 + thing2
16017}
16018";
16019        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16020        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16021            panic!("expected an expression statement");
16022        };
16023        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16024        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16025            &mut ast,
16026            &source_ref,
16027            AstMutateCommand::AddVariableDeclaration {
16028                prefix: "thing".to_owned(),
16029            },
16030        )
16031        .unwrap();
16032        let AstMutateCommandReturn::Name(name) = cmd_return else {
16033            panic!("expected a generated name");
16034        };
16035        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16036    }
16037
16038    #[test]
16039    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16040        let code = "\
16041x = 1
16042y = if x > 0 {
16043  q1 = 1
16044  foo(q1)
16045  q1
16046} else {
16047  2
16048}
16049";
16050        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16051        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16052            panic!("expected an expression statement");
16053        };
16054        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16055        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16056            &mut ast,
16057            &source_ref,
16058            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16059        )
16060        .unwrap();
16061        let AstMutateCommandReturn::Name(name) = cmd_return else {
16062            panic!("expected a generated name");
16063        };
16064        assert_eq!(name, "q2");
16065        let body = &then_block_at(&ast, 1).body;
16066        assert_eq!(body.len(), 3);
16067        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16068            panic!("expected the expression statement to become a variable declaration");
16069        };
16070        assert_eq!(decl.name(), "q2");
16071        // Siblings are untouched.
16072        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16073            panic!("expected a variable declaration");
16074        };
16075        assert_eq!(first.name(), "q1");
16076        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16077    }
16078
16079    #[test]
16080    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16081        // Before the shared body traversal, MutateBodyItem::Delete was
16082        // silently dropped inside if-expression branch blocks.
16083        let code = "\
16084y = if true {
16085  a = 1
16086  // keep me
16087  b = 2
16088  a + b
16089} else {
16090  2
16091}
16092";
16093        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16094        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16095            panic!("expected a variable declaration");
16096        };
16097        assert_eq!(b_decl.name(), "b");
16098        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16099        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16100        let body = &then_block_at(&ast, 0).body;
16101        assert_eq!(body.len(), 2, "expected b to be deleted");
16102        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16103            panic!("expected a variable declaration");
16104        };
16105        assert_eq!(first.name(), "a");
16106        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16107            panic!("expected the tail expression to remain");
16108        };
16109        assert!(
16110            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16111            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16112            body[1].get_comments()
16113        );
16114    }
16115}