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::CircleCtor;
39use crate::front::ControlPointSplineCtor;
40use crate::front::Distance;
41use crate::front::EqualRadius;
42use crate::front::Error;
43use crate::front::ExecResult;
44use crate::front::FixedPoint;
45use crate::front::Freedom;
46use crate::front::LinesEqualLength;
47use crate::front::Midpoint;
48use crate::front::Object;
49use crate::front::Parallel;
50use crate::front::Perpendicular;
51use crate::front::PointCtor;
52use crate::front::Symmetric;
53use crate::front::Tangent;
54use crate::frontend::api::CapSource;
55use crate::frontend::api::Expr;
56use crate::frontend::api::FileId;
57use crate::frontend::api::Number;
58use crate::frontend::api::ObjectId;
59use crate::frontend::api::ObjectKind;
60use crate::frontend::api::Plane;
61use crate::frontend::api::ProjectId;
62use crate::frontend::api::RestoreSketchCheckpointOutcome;
63use crate::frontend::api::SceneGraph;
64use crate::frontend::api::SceneGraphDelta;
65use crate::frontend::api::SketchCheckpointId;
66use crate::frontend::api::SourceDelta;
67use crate::frontend::api::SourceRef;
68use crate::frontend::api::SourceRefRange;
69use crate::frontend::api::Version;
70use crate::frontend::api::WallSource;
71use crate::frontend::modify::find_defined_names;
72use crate::frontend::modify::next_free_name;
73use crate::frontend::modify::next_free_name_with_padding;
74use crate::frontend::sketch::Coincident;
75use crate::frontend::sketch::Constraint;
76use crate::frontend::sketch::ConstraintLabelPositionEdit;
77use crate::frontend::sketch::ConstraintSegment;
78use crate::frontend::sketch::Diameter;
79use crate::frontend::sketch::ExistingSegmentCtor;
80use crate::frontend::sketch::Horizontal;
81use crate::frontend::sketch::LineCtor;
82use crate::frontend::sketch::Point2d;
83use crate::frontend::sketch::Radius;
84use crate::frontend::sketch::Segment;
85use crate::frontend::sketch::SegmentCtor;
86use crate::frontend::sketch::SketchApi;
87use crate::frontend::sketch::SketchCtor;
88use crate::frontend::sketch::Vertical;
89use crate::frontend::traverse::MutateBodyItem;
90use crate::frontend::traverse::TraversalReturn;
91use crate::frontend::traverse::Visitor;
92use crate::frontend::traverse::dfs_mut;
93use crate::id::IncIdGenerator;
94use crate::parsing::ast::types as ast;
95use crate::parsing::ast::types::NodePathExt;
96use crate::pretty::NumericSuffix;
97use crate::std::constraints::LinesAtAngleKind;
98use crate::walk::NodeMut;
99use crate::walk::Visitable;
100
101pub(crate) mod api;
102pub(crate) mod modify;
103pub(crate) mod sketch;
104
105pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
106
107#[derive(Debug, Clone)]
108struct SketchCheckpoint {
109    id: SketchCheckpointId,
110    source: SourceDelta,
111    program: Program,
112    scene_graph: SceneGraph,
113    exec_outcome: ExecOutcome,
114    point_freedom_cache: HashMap<ObjectId, Freedom>,
115    mock_memory: Option<SketchModeState>,
116}
117mod traverse;
118pub(crate) mod trim;
119
120struct ArcSizeConstraintParams {
121    points: Vec<ObjectId>,
122    function_name: &'static str,
123    value: f64,
124    units: NumericSuffix,
125    label_position: Option<Point2d<Number>>,
126    constraint_type_name: &'static str,
127}
128
129const POINT_FN: &str = "point";
130const POINT_AT_PARAM: &str = "at";
131const LINE_FN: &str = "line";
132const LINE_VARIABLE: &str = "line";
133const LINE_START_PARAM: &str = "start";
134const LINE_END_PARAM: &str = "end";
135const ARC_FN: &str = "arc";
136const ARC_VARIABLE: &str = "arc";
137const ARC_START_PARAM: &str = "start";
138const ARC_END_PARAM: &str = "end";
139const ARC_CENTER_PARAM: &str = "center";
140const CIRCLE_FN: &str = "circle";
141const CIRCLE_VARIABLE: &str = "circle";
142const CIRCLE_START_PARAM: &str = "start";
143const CIRCLE_CENTER_PARAM: &str = "center";
144const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
145const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
146const LABEL_POSITION_PARAM: &str = "labelPosition";
147
148const COINCIDENT_FN: &str = "coincident";
149const DIAMETER_FN: &str = "diameter";
150const DISTANCE_FN: &str = "distance";
151const FIXED_FN: &str = "fixed";
152const ANGLE_FN: &str = "angle";
153const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
154const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
155const EQUAL_LENGTH_FN: &str = "equalLength";
156const EQUAL_RADIUS_FN: &str = "equalRadius";
157const HORIZONTAL_FN: &str = "horizontal";
158const MIDPOINT_FN: &str = "midpoint";
159const MIDPOINT_POINT_PARAM: &str = "point";
160const RADIUS_FN: &str = "radius";
161const SYMMETRIC_FN: &str = "symmetric";
162const SYMMETRIC_AXIS_PARAM: &str = "axis";
163const TANGENT_FN: &str = "tangent";
164const VERTICAL_FN: &str = "vertical";
165
166const LINE_PROPERTY_START: &str = "start";
167const LINE_PROPERTY_END: &str = "end";
168
169const ARC_PROPERTY_START: &str = "start";
170const ARC_PROPERTY_END: &str = "end";
171const ARC_PROPERTY_CENTER: &str = "center";
172const CIRCLE_PROPERTY_START: &str = "start";
173const CIRCLE_PROPERTY_CENTER: &str = "center";
174const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
175const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
176
177const CONSTRUCTION_PARAM: &str = "construction";
178
179#[derive(Debug, Clone, Copy)]
180enum EditDeleteKind {
181    Edit,
182    DeleteNonSketch,
183}
184
185/// Options that control how an edit is re-executed and written back.
186struct ExecuteAfterEditOptions {
187    segment_ids_edited: AhashIndexSet<ObjectId>,
188    edit_kind: EditDeleteKind,
189    commit_solved_initial_guesses: bool,
190}
191
192impl EditDeleteKind {
193    /// Returns true if this edit is any type of deletion.
194    fn is_delete(&self) -> bool {
195        match self {
196            EditDeleteKind::Edit => false,
197            EditDeleteKind::DeleteNonSketch => true,
198        }
199    }
200
201    fn to_change_kind(self) -> ChangeKind {
202        match self {
203            EditDeleteKind::Edit => ChangeKind::Edit,
204            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
205        }
206    }
207}
208
209#[derive(Debug, Clone, Copy)]
210enum ChangeKind {
211    Add,
212    Edit,
213    Delete,
214    None,
215}
216
217#[derive(Debug, Clone, Serialize, ts_rs::TS)]
218#[ts(export, export_to = "FrontendApi.ts")]
219#[serde(tag = "type")]
220pub enum SetProgramOutcome {
221    #[serde(rename_all = "camelCase")]
222    Success {
223        scene_graph: Box<SceneGraph>,
224        exec_outcome: Box<ExecOutcome>,
225        checkpoint_id: Option<SketchCheckpointId>,
226    },
227    #[serde(rename_all = "camelCase")]
228    ExecFailure { error: Box<KclErrorWithOutputs> },
229}
230
231/// Options for a sketch segment edit that participates in drag solving.
232pub struct EditSegmentsOptions {
233    /// Narrows which edited scene objects receive temporary fixed constraints.
234    ///
235    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
236    /// disables those fixed constraints, which is useful for semantic edits such
237    /// as toggling construction state.
238    pub anchor_segment_ids: Option<Vec<ObjectId>>,
239    /// Hidden fixed cursor points that the referenced segment bodies must pass
240    /// through during solve.
241    pub drag_anchors: Vec<SegmentDragAnchor>,
242    /// Constraint label positions to write in the same AST transaction as the
243    /// segment edits. Label positions do not participate in solving.
244    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
245    /// Whether solver-updated initial guesses should be written back to KCL.
246    pub commit_solved_initial_guesses: bool,
247}
248
249/// Options for a distance-constraint label edit during sketch dragging.
250pub struct EditDistanceConstraintLabelPositionOptions {
251    /// Edited scene objects to keep anchored while previewing the label edit.
252    pub anchor_segment_ids: Vec<ObjectId>,
253    /// Whether solver-updated initial guesses should be written back to KCL.
254    pub commit_solved_initial_guesses: bool,
255}
256
257#[derive(Debug, Clone)]
258struct SolidAstReference {
259    variable_name: String,
260    output_index: Option<usize>,
261}
262
263#[derive(Debug, Clone)]
264pub struct FrontendState {
265    program: Program,
266    scene_graph: SceneGraph,
267    /// Lightweight map from engine solid IDs to their latest KCL references.
268    solid_references: HashMap<Uuid, SolidAstReference>,
269    /// Stores the last known freedom value for each point object.
270    /// This allows us to preserve freedom values when freedom analysis isn't run.
271    point_freedom_cache: HashMap<ObjectId, Freedom>,
272    /// One-shot drag anchors for the next segment edit. These ids define which
273    /// edited points/segments become temporary fixed constraints during solve.
274    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
275    /// One-shot segment-body drag anchors for the next segment edit. These add
276    /// a temporary solver point on the dragged segment that follows the cursor.
277    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
278    /// One-shot constraint label edits to apply with the next segment edit.
279    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
280    /// One-shot override for whether the next edit commits solver-updated
281    /// initial guesses back into KCL. Drag previews keep this off so only the
282    /// explicit drag edit feeds the next solve.
283    next_edit_commits_solver_solutions: Option<bool>,
284    sketch_checkpoints: VecDeque<SketchCheckpoint>,
285    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
286}
287
288impl Default for FrontendState {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294impl FrontendState {
295    pub fn new() -> Self {
296        Self {
297            program: Program::empty(),
298            scene_graph: SceneGraph {
299                project: ProjectId(0),
300                file: FileId(0),
301                version: Version(0),
302                objects: Default::default(),
303                settings: Default::default(),
304                sketch_mode: Default::default(),
305            },
306            solid_references: HashMap::new(),
307            point_freedom_cache: HashMap::new(),
308            next_drag_anchor_segment_ids: None,
309            next_segment_drag_anchors: None,
310            next_constraint_label_edits: None,
311            next_edit_commits_solver_solutions: None,
312            sketch_checkpoints: VecDeque::new(),
313            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
314        }
315    }
316
317    /// Get a reference to the scene graph
318    pub fn scene_graph(&self) -> &SceneGraph {
319        &self.scene_graph
320    }
321
322    pub fn default_length_unit(&self) -> UnitLength {
323        self.program
324            .meta_settings()
325            .ok()
326            .flatten()
327            .map(|settings| settings.default_length_units)
328            .unwrap_or(UnitLength::Millimeters)
329    }
330
331    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
332        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
333
334        let checkpoint = SketchCheckpoint {
335            id: checkpoint_id,
336            source: SourceDelta {
337                text: source_from_ast(&self.program.ast),
338            },
339            program: self.program.clone(),
340            scene_graph: self.scene_graph.clone(),
341            exec_outcome,
342            point_freedom_cache: self.point_freedom_cache.clone(),
343            mock_memory: read_old_memory().await,
344        };
345
346        self.sketch_checkpoints.push_back(checkpoint);
347        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
348            self.sketch_checkpoints.pop_front();
349        }
350
351        Ok(checkpoint_id)
352    }
353
354    /// Edit sketch segments with optional drag-solve overrides.
355    ///
356    /// Drag anchors add hidden fixed cursor points and constrain the referenced
357    /// segment bodies to pass through them, which lets body drags use the same
358    /// anchor model without pinning all child points. Preview callers disable
359    /// solver writeback so solved geometry can be returned without feeding every
360    /// solver value back into KCL.
361    pub async fn edit_segments_with_options(
362        &mut self,
363        ctx: &ExecutorContext,
364        version: Version,
365        sketch: ObjectId,
366        segments: Vec<ExistingSegmentCtor>,
367        options: EditSegmentsOptions,
368    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
369        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
370            self.next_drag_anchor_segment_ids
371                .replace(anchor_ids.into_iter().collect())
372        });
373        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
374        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
375        let previous_commit_mode = self
376            .next_edit_commits_solver_solutions
377            .replace(options.commit_solved_initial_guesses);
378        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
379        if let Some(previous_anchor_ids) = previous_anchor_ids {
380            self.next_drag_anchor_segment_ids = previous_anchor_ids;
381        }
382        self.next_segment_drag_anchors = previous_drag_anchors;
383        self.next_constraint_label_edits = previous_constraint_label_edits;
384        self.next_edit_commits_solver_solutions = previous_commit_mode;
385        result
386    }
387
388    /// Edit a distance-constraint label position with optional solver writeback.
389    ///
390    /// Drag previews set `commit_solved_initial_guesses` to false so label
391    /// placement can be previewed against solved geometry without advancing
392    /// persistent KCL state until drag completion.
393    pub async fn edit_distance_constraint_label_position_with_options(
394        &mut self,
395        ctx: &ExecutorContext,
396        version: Version,
397        sketch: ObjectId,
398        constraint_id: ObjectId,
399        label_position: Point2d<Number>,
400        options: EditDistanceConstraintLabelPositionOptions,
401    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
402        let previous_commit_mode = self
403            .next_edit_commits_solver_solutions
404            .replace(options.commit_solved_initial_guesses);
405        let result = SketchApi::edit_distance_constraint_label_position(
406            self,
407            ctx,
408            version,
409            sketch,
410            constraint_id,
411            label_position,
412            options.anchor_segment_ids,
413        )
414        .await;
415        self.next_edit_commits_solver_solutions = previous_commit_mode;
416        result
417    }
418
419    pub async fn restore_sketch_checkpoint(
420        &mut self,
421        checkpoint_id: SketchCheckpointId,
422    ) -> api::Result<RestoreSketchCheckpointOutcome> {
423        let checkpoint = self
424            .sketch_checkpoints
425            .iter()
426            .find(|checkpoint| checkpoint.id == checkpoint_id)
427            .cloned()
428            .ok_or_else(|| Error {
429                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
430            })?;
431
432        self.program = checkpoint.program;
433        self.scene_graph = checkpoint.scene_graph.clone();
434        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
435        self.point_freedom_cache = checkpoint.point_freedom_cache;
436        self.next_drag_anchor_segment_ids = None;
437        self.next_segment_drag_anchors = None;
438        self.next_constraint_label_edits = None;
439        self.next_edit_commits_solver_solutions = None;
440
441        if let Some(mock_memory) = checkpoint.mock_memory {
442            write_old_memory(mock_memory).await;
443        } else {
444            clear_mem_cache().await;
445        }
446
447        Ok(RestoreSketchCheckpointOutcome {
448            source_delta: checkpoint.source,
449            scene_graph_delta: SceneGraphDelta {
450                new_graph: self.scene_graph_for_ui(),
451                new_objects: Vec::new(),
452                invalidates_ids: true,
453                exec_outcome: checkpoint.exec_outcome,
454            },
455        })
456    }
457
458    pub fn clear_sketch_checkpoints(&mut self) {
459        self.sketch_checkpoints.clear();
460    }
461    fn scene_graph_for_ui(&self) -> SceneGraph {
462        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
463            matches!(
464                object.kind,
465                ObjectKind::Segment {
466                    segment: Segment::ControlPointSpline(_)
467                }
468            )
469        });
470
471        if !has_control_point_splines {
472            return self.scene_graph.clone();
473        }
474
475        let hidden_constraint_ids = self
476            .scene_graph
477            .objects
478            .iter()
479            .filter_map(|object| match &object.kind {
480                ObjectKind::Constraint {
481                    constraint: Constraint::Coincident(coincident),
482                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
483                    Some(object.id)
484                }
485                _ => None,
486            })
487            .collect::<HashSet<_>>();
488
489        if hidden_constraint_ids.is_empty() {
490            return self.scene_graph.clone();
491        }
492
493        let mut scene_graph = self.scene_graph.clone();
494        for object in &mut scene_graph.objects {
495            match &mut object.kind {
496                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
497                    object.kind = ObjectKind::Nil;
498                }
499                ObjectKind::Sketch(sketch) => {
500                    sketch
501                        .constraints
502                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
503                }
504                _ => {}
505            }
506        }
507
508        scene_graph
509    }
510}
511
512fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
513    let mut first_owner_id = None;
514    for segment_id in coincident.segment_ids() {
515        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
516            return false;
517        };
518
519        match first_owner_id {
520            Some(first_owner_id) if first_owner_id != owner_id => return false,
521            Some(_) => {}
522            None => first_owner_id = Some(owner_id),
523        }
524    }
525
526    first_owner_id.is_some()
527}
528
529fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
530    let object = scene_graph.objects.get(segment_id.0)?;
531    let ObjectKind::Segment { segment } = &object.kind else {
532        return None;
533    };
534
535    match segment {
536        Segment::ControlPointSpline(_) => Some(segment_id),
537        Segment::Point(point) => point
538            .owner
539            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
540        Segment::Line(line) => line
541            .owner
542            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
543        _ => None,
544    }
545}
546
547fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
548    matches!(
549        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
550        Some(ObjectKind::Segment {
551            segment: Segment::ControlPointSpline(_)
552        })
553    )
554}
555
556fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
557    let experimental_features_allowed = program
558        .meta_settings()
559        .ok()
560        .flatten()
561        .map(|settings| settings.experimental_features == WarningLevel::Allow)
562        .unwrap_or(false);
563    if experimental_features_allowed {
564        return Ok(program.clone());
565    }
566
567    program.change_experimental_features(Some(WarningLevel::Allow))
568}
569
570impl SketchApi for FrontendState {
571    async fn execute_mock(
572        &mut self,
573        ctx: &ExecutorContext,
574        _version: Version,
575        sketch: ObjectId,
576    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
577        let sketch_block_ref =
578            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
579
580        let mut truncated_program = self.program.clone();
581        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
582            .map_err(KclErrorWithOutputs::no_outputs)?;
583
584        // Execute.
585        let outcome = ctx
586            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
587            .await?;
588        let new_source = source_from_ast(&self.program.ast);
589        let src_delta = SourceDelta { text: new_source };
590        // MockConfig::default() has freedom_analysis: true
591        let outcome = self.update_state_after_exec(outcome, true);
592        let scene_graph_delta = SceneGraphDelta {
593            new_graph: self.scene_graph.clone(),
594            new_objects: Default::default(),
595            invalidates_ids: false,
596            exec_outcome: outcome,
597        };
598        Ok((src_delta, scene_graph_delta))
599    }
600
601    async fn new_sketch(
602        &mut self,
603        ctx: &ExecutorContext,
604        _project: ProjectId,
605        _file: FileId,
606        _version: Version,
607        args: SketchCtor,
608    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
609        // TODO: Check version.
610
611        let mut new_ast = self.program.ast.clone();
612        // Create updated KCL source from args.
613        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
614            .map_err(KclErrorWithOutputs::no_outputs)?;
615        let mut defined_names = find_defined_names(&new_ast);
616        let is_face_of_expr = matches!(
617            &plane_ast,
618            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
619        );
620        if is_face_of_expr {
621            let face_name = next_free_name_with_padding("face", &defined_names)
622                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
623            let face_decl = ast::VariableDeclaration::new(
624                ast::VariableDeclarator::new(&face_name, plane_ast),
625                ast::ItemVisibility::Default,
626                ast::VariableKind::Const,
627            );
628            new_ast
629                .body
630                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
631                    face_decl,
632                ))));
633            defined_names.insert(face_name.clone());
634            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
635        }
636        let sketch_ast = ast::SketchBlock {
637            arguments: vec![ast::LabeledArg {
638                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
639                arg: plane_ast,
640            }],
641            body: Default::default(),
642            is_being_edited: false,
643            non_code_meta: Default::default(),
644            digest: None,
645        };
646        // Add a sketch block as a variable declaration directly, avoiding
647        // source-range mutation on a no-src node.
648        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
649            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
650        let sketch_decl = ast::VariableDeclaration::new(
651            ast::VariableDeclarator::new(
652                &sketch_name,
653                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
654            ),
655            ast::ItemVisibility::Default,
656            ast::VariableKind::Const,
657        );
658        new_ast
659            .body
660            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
661                sketch_decl,
662            ))));
663        // Convert to string source to create real source ranges.
664        let new_source = source_from_ast(&new_ast);
665        // Parse the new source.
666        let new_program = parse_frontend_mutation_source(
667            &new_source,
668            "Error parsing KCL source after adding sketch",
669            "No AST produced after adding sketch",
670        )?;
671
672        // Make sure to only set this if there are no errors.
673        self.program = new_program.clone();
674
675        // We need to do an engine execute so that the plane object gets created
676        // and is cached.
677        let outcome = ctx.run_with_caching(new_program.clone()).await?;
678        let freedom_analysis_ran = true;
679
680        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
681
682        let Some(sketch_id) = self
683            .scene_graph
684            .objects
685            .iter()
686            .filter_map(|object| match object.kind {
687                ObjectKind::Sketch(_) => Some(object.id),
688                _ => None,
689            })
690            .max_by_key(|id| id.0)
691        else {
692            return Err(KclErrorWithOutputs::from_error_outcome(
693                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
694                outcome,
695            ));
696        };
697        // Store the object in the scene.
698        self.scene_graph.sketch_mode = Some(sketch_id);
699
700        let src_delta = SourceDelta { text: new_source };
701        let scene_graph_delta = SceneGraphDelta {
702            new_graph: self.scene_graph_for_ui(),
703            invalidates_ids: false,
704            new_objects: vec![sketch_id],
705            exec_outcome: outcome,
706        };
707        Ok((src_delta, scene_graph_delta, sketch_id))
708    }
709
710    async fn edit_sketch(
711        &mut self,
712        ctx: &ExecutorContext,
713        _project: ProjectId,
714        _file: FileId,
715        _version: Version,
716        sketch: ObjectId,
717    ) -> ExecResult<SceneGraphDelta> {
718        // TODO: Check version.
719
720        // Look up existing sketch.
721        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
722            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
723        })?;
724        let ObjectKind::Sketch(_) = &sketch_object.kind else {
725            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
726                "Object is not a sketch, it is {}",
727                sketch_object.kind.human_friendly_kind_with_article()
728            ))));
729        };
730        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
731
732        // Enter sketch mode by setting the sketch_mode.
733        self.scene_graph.sketch_mode = Some(sketch);
734
735        // Truncate after the sketch block for mock execution.
736        let mut truncated_program = self.program.clone();
737        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
738            .map_err(KclErrorWithOutputs::no_outputs)?;
739
740        // Execute in mock mode to ensure state is up to date. The caller will
741        // want freedom analysis to display segments correctly.
742        let outcome = ctx
743            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
744            .await?;
745
746        // MockConfig::default() has freedom_analysis: true
747        let outcome = self.update_state_after_exec(outcome, true);
748        let scene_graph_delta = SceneGraphDelta {
749            new_graph: self.scene_graph_for_ui(),
750            invalidates_ids: false,
751            new_objects: Vec::new(),
752            exec_outcome: outcome,
753        };
754        Ok(scene_graph_delta)
755    }
756
757    async fn exit_sketch(
758        &mut self,
759        ctx: &ExecutorContext,
760        _version: Version,
761        sketch: ObjectId,
762    ) -> ExecResult<SceneGraph> {
763        // TODO: Check version.
764        #[cfg(not(target_arch = "wasm32"))]
765        let _ = sketch;
766        #[cfg(target_arch = "wasm32")]
767        if self.scene_graph.sketch_mode != Some(sketch) {
768            web_sys::console::warn_1(
769                &format!(
770                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
771                    self.scene_graph.sketch_mode
772                )
773                .into(),
774            );
775        }
776        self.scene_graph.sketch_mode = None;
777
778        // Execute.
779        let outcome = ctx.run_with_caching(self.program.clone()).await?;
780
781        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
782        self.update_state_after_exec(outcome, false);
783
784        Ok(self.scene_graph_for_ui())
785    }
786
787    async fn delete_sketch(
788        &mut self,
789        ctx: &ExecutorContext,
790        _version: Version,
791        sketch: ObjectId,
792    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
793        // TODO: Check version.
794
795        let mut new_ast = self.program.ast.clone();
796
797        // Look up existing sketch.
798        let sketch_id = sketch;
799        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
800            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
801        })?;
802        let ObjectKind::Sketch(_) = &sketch_object.kind else {
803            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
804                "Object is not a sketch, it is {}",
805                sketch_object.kind.human_friendly_kind_with_article(),
806            ))));
807        };
808
809        // Modify the AST to remove the sketch.
810        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
811            .map_err(KclErrorWithOutputs::no_outputs)?;
812
813        self.execute_after_delete_sketch(ctx, &mut new_ast).await
814    }
815
816    async fn add_segment(
817        &mut self,
818        ctx: &ExecutorContext,
819        _version: Version,
820        sketch: ObjectId,
821        segment: SegmentCtor,
822        _label: Option<String>,
823    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
824        // TODO: Check version.
825        match segment {
826            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
827            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
828            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
829            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
830            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
831        }
832    }
833
834    async fn edit_segments(
835        &mut self,
836        ctx: &ExecutorContext,
837        _version: Version,
838        sketch: ObjectId,
839        segments: Vec<ExistingSegmentCtor>,
840    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
841        // TODO: Check version.
842        let sketch_block_ref =
843            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
844
845        let mut new_ast = self.program.ast.clone();
846        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
847        let mut invalidates_ids = false;
848
849        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
850        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
851        for segment in &segments {
852            edited_segment_ids.insert(segment.id);
853            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
854                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
855                && let ObjectKind::Segment {
856                    segment: Segment::ControlPointSpline(existing_spline),
857                } = &existing_object.kind
858                && existing_spline.controls.len() != new_ctor.points.len()
859            {
860                invalidates_ids = true;
861            }
862        }
863        let drag_anchor_segment_ids = self
864            .next_drag_anchor_segment_ids
865            .take()
866            .unwrap_or_else(|| edited_segment_ids.clone());
867        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
868        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
869
870        // Preprocess segments into a final_edits vector to handle if segments contains:
871        // - edit start point of line1 (as SegmentCtor::Point)
872        // - edit end point of line1 (as SegmentCtor::Point)
873        //
874        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
875        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
876        //
877        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
878        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
879        // so the above example would result in a single line1 edit:
880        // - the first start point edit creates a new line edit entry in final_edits
881        // - the second end point edit finds this line edit and mutates the end position only.
882        //
883        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
884        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
885
886        for segment in segments {
887            let segment_id = segment.id;
888            match segment.ctor {
889                SegmentCtor::Point(ctor) => {
890                    // Find the owner, if any (point -> line / arc)
891                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
892                        && let ObjectKind::Segment { segment } = &segment_object.kind
893                        && let Segment::Point(point) = segment
894                        && let Some(owner_id) = point.owner
895                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
896                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
897                    {
898                        match owner_segment {
899                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
900                                if let Some(existing) = final_edits.get_mut(&owner_id) {
901                                    let SegmentCtor::Line(line_ctor) = existing else {
902                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
903                                            "Internal: Expected line ctor for owner, but found {}",
904                                            existing.human_friendly_kind_with_article()
905                                        ))));
906                                    };
907                                    // Line owner is already in final_edits -> apply this point edit
908                                    if line.start == segment_id {
909                                        line_ctor.start = ctor.position;
910                                    } else {
911                                        line_ctor.end = ctor.position;
912                                    }
913                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
914                                    // Line owner is not in final_edits yet -> create it
915                                    let mut line_ctor = line_ctor.clone();
916                                    if line.start == segment_id {
917                                        line_ctor.start = ctor.position;
918                                    } else {
919                                        line_ctor.end = ctor.position;
920                                    }
921                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
922                                } else {
923                                    // This should never run..
924                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
925                                        "Internal: Line does not have line ctor, but found {}",
926                                        line.ctor.human_friendly_kind_with_article()
927                                    ))));
928                                }
929                                continue;
930                            }
931                            Segment::Arc(arc)
932                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
933                            {
934                                if let Some(existing) = final_edits.get_mut(&owner_id) {
935                                    let SegmentCtor::Arc(arc_ctor) = existing else {
936                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
937                                            "Internal: Expected arc ctor for owner, but found {}",
938                                            existing.human_friendly_kind_with_article()
939                                        ))));
940                                    };
941                                    if arc.start == segment_id {
942                                        arc_ctor.start = ctor.position;
943                                    } else if arc.end == segment_id {
944                                        arc_ctor.end = ctor.position;
945                                    } else {
946                                        arc_ctor.center = ctor.position;
947                                    }
948                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
949                                    let mut arc_ctor = arc_ctor.clone();
950                                    if arc.start == segment_id {
951                                        arc_ctor.start = ctor.position;
952                                    } else if arc.end == segment_id {
953                                        arc_ctor.end = ctor.position;
954                                    } else {
955                                        arc_ctor.center = ctor.position;
956                                    }
957                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
958                                } else {
959                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
960                                        "Internal: Arc does not have arc ctor, but found {}",
961                                        arc.ctor.human_friendly_kind_with_article()
962                                    ))));
963                                }
964                                continue;
965                            }
966                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
967                                if let Some(existing) = final_edits.get_mut(&owner_id) {
968                                    let SegmentCtor::Circle(circle_ctor) = existing else {
969                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
970                                            "Internal: Expected circle ctor for owner, but found {}",
971                                            existing.human_friendly_kind_with_article()
972                                        ))));
973                                    };
974                                    if circle.start == segment_id {
975                                        circle_ctor.start = ctor.position;
976                                    } else {
977                                        circle_ctor.center = ctor.position;
978                                    }
979                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
980                                    let mut circle_ctor = circle_ctor.clone();
981                                    if circle.start == segment_id {
982                                        circle_ctor.start = ctor.position;
983                                    } else {
984                                        circle_ctor.center = ctor.position;
985                                    }
986                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
987                                } else {
988                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
989                                        "Internal: Circle does not have circle ctor, but found {}",
990                                        circle.ctor.human_friendly_kind_with_article()
991                                    ))));
992                                }
993                                continue;
994                            }
995                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
996                                let Some(control_index) =
997                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
998                                else {
999                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1000                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1001                                    ))));
1002                                };
1003                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1004                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1005                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1006                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1007                                            existing.human_friendly_kind_with_article()
1008                                        ))));
1009                                    };
1010                                    spline_ctor.points[control_index] = ctor.position;
1011                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1012                                    let mut spline_ctor = spline_ctor.clone();
1013                                    spline_ctor.points[control_index] = ctor.position;
1014                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1015                                } else {
1016                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1017                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1018                                        spline.ctor.human_friendly_kind_with_article()
1019                                    ))));
1020                                }
1021                                continue;
1022                            }
1023                            _ => {}
1024                        }
1025                    }
1026
1027                    // No owner, it's an individual point
1028                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1029                }
1030                SegmentCtor::Line(ctor) => {
1031                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1032                }
1033                SegmentCtor::Arc(ctor) => {
1034                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1035                }
1036                SegmentCtor::Circle(ctor) => {
1037                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1038                }
1039                SegmentCtor::ControlPointSpline(ctor) => {
1040                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1041                }
1042            }
1043        }
1044
1045        for (segment_id, ctor) in final_edits {
1046            match ctor {
1047                SegmentCtor::Point(ctor) => self
1048                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1049                    .map_err(KclErrorWithOutputs::no_outputs)?,
1050                SegmentCtor::Line(ctor) => self
1051                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1052                    .map_err(KclErrorWithOutputs::no_outputs)?,
1053                SegmentCtor::Arc(ctor) => self
1054                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1055                    .map_err(KclErrorWithOutputs::no_outputs)?,
1056                SegmentCtor::Circle(ctor) => self
1057                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1058                    .map_err(KclErrorWithOutputs::no_outputs)?,
1059                SegmentCtor::ControlPointSpline(ctor) => self
1060                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1061                    .map_err(KclErrorWithOutputs::no_outputs)?,
1062            }
1063        }
1064        for edit in constraint_label_edits {
1065            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1066                .map_err(KclErrorWithOutputs::no_outputs)?;
1067        }
1068        let (source_delta, mut scene_graph_delta) = self
1069            .execute_after_edit(
1070                ctx,
1071                sketch,
1072                sketch_block_ref,
1073                &mut new_ast,
1074                ExecuteAfterEditOptions {
1075                    segment_ids_edited: drag_anchor_segment_ids,
1076                    edit_kind: EditDeleteKind::Edit,
1077                    commit_solved_initial_guesses,
1078                },
1079            )
1080            .await?;
1081        if invalidates_ids {
1082            scene_graph_delta.invalidates_ids = true;
1083        }
1084        Ok((source_delta, scene_graph_delta))
1085    }
1086
1087    async fn delete_objects(
1088        &mut self,
1089        ctx: &ExecutorContext,
1090        _version: Version,
1091        sketch: ObjectId,
1092        constraint_ids: Vec<ObjectId>,
1093        segment_ids: Vec<ObjectId>,
1094    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1095        // TODO: Check version.
1096        let sketch_block_ref =
1097            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1098
1099        // Deduplicate IDs.
1100        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1101        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1102
1103        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1104        // also delete the point, as well as other points that are owned by the owner.
1105        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1106
1107        for segment_id in segment_ids_set.iter().copied() {
1108            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1109                let ObjectKind::Segment { segment } = &segment_object.kind else {
1110                    return None;
1111                };
1112                match segment {
1113                    Segment::Point(point) => point.owner,
1114                    Segment::Line(line) => line.owner,
1115                    _ => None,
1116                }
1117            });
1118
1119            if let Some(owner_id) = owner_id
1120                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1121                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1122                && matches!(
1123                    owner_segment,
1124                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1125                )
1126            {
1127                // segment is owned -> delete the owner
1128                resolved_segment_ids_to_delete.insert(owner_id);
1129            } else {
1130                // segment is not owned by anything -> can be deleted
1131                resolved_segment_ids_to_delete.insert(segment_id);
1132            }
1133        }
1134        let referenced_constraint_ids = self
1135            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1136            .map_err(KclErrorWithOutputs::no_outputs)?;
1137
1138        let mut new_ast = self.program.ast.clone();
1139
1140        for constraint_id in referenced_constraint_ids {
1141            if constraint_ids_set.contains(&constraint_id) {
1142                continue;
1143            }
1144
1145            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1146                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1147            })?;
1148            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1149                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1150                    "Object is not a constraint, it is {}",
1151                    constraint_object.kind.human_friendly_kind_with_article()
1152                ))));
1153            };
1154
1155            match constraint {
1156                Constraint::Coincident(coincident) => {
1157                    let remaining_segments =
1158                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1159
1160                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1161                    if remaining_segments.len() >= 2 {
1162                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1163                            .map_err(KclErrorWithOutputs::no_outputs)?;
1164                    } else {
1165                        constraint_ids_set.insert(constraint_id);
1166                    }
1167                }
1168                Constraint::EqualRadius(equal_radius) => {
1169                    let remaining_input = equal_radius
1170                        .input
1171                        .iter()
1172                        .copied()
1173                        .filter(|segment_id| {
1174                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1175                        })
1176                        .collect::<Vec<_>>();
1177
1178                    if remaining_input.len() >= 2 {
1179                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1180                            .map_err(KclErrorWithOutputs::no_outputs)?;
1181                    } else {
1182                        constraint_ids_set.insert(constraint_id);
1183                    }
1184                }
1185                Constraint::LinesEqualLength(lines_equal_length) => {
1186                    let remaining_lines = lines_equal_length
1187                        .lines
1188                        .iter()
1189                        .copied()
1190                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1191                        .collect::<Vec<_>>();
1192
1193                    // Equal length constraint is only valid with at least 2 lines
1194                    if remaining_lines.len() >= 2 {
1195                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1196                            .map_err(KclErrorWithOutputs::no_outputs)?;
1197                    } else {
1198                        constraint_ids_set.insert(constraint_id);
1199                    }
1200                }
1201                Constraint::Parallel(parallel) => {
1202                    let remaining_lines = parallel
1203                        .lines
1204                        .iter()
1205                        .copied()
1206                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1207                        .collect::<Vec<_>>();
1208
1209                    if remaining_lines.len() >= 2 {
1210                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1211                            .map_err(KclErrorWithOutputs::no_outputs)?;
1212                    } else {
1213                        constraint_ids_set.insert(constraint_id);
1214                    }
1215                }
1216                Constraint::Horizontal(Horizontal::Points { points }) => {
1217                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1218
1219                    if remaining_points.len() >= 2 {
1220                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1221                            .map_err(KclErrorWithOutputs::no_outputs)?;
1222                    } else {
1223                        constraint_ids_set.insert(constraint_id);
1224                    }
1225                }
1226                Constraint::Vertical(Vertical::Points { points }) => {
1227                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1228
1229                    if remaining_points.len() >= 2 {
1230                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1231                            .map_err(KclErrorWithOutputs::no_outputs)?;
1232                    } else {
1233                        constraint_ids_set.insert(constraint_id);
1234                    }
1235                }
1236                Constraint::Fixed(fixed) => {
1237                    if fixed.points.iter().any(|fixed_point| {
1238                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1239                    }) {
1240                        constraint_ids_set.insert(constraint_id);
1241                    }
1242                }
1243                _ => {
1244                    // All other constraint types: if referenced by a segment -> delete the constraint
1245                    constraint_ids_set.insert(constraint_id);
1246                }
1247            }
1248        }
1249
1250        for constraint_id in constraint_ids_set {
1251            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1252                .map_err(KclErrorWithOutputs::no_outputs)?;
1253        }
1254        for segment_id in resolved_segment_ids_to_delete {
1255            self.delete_segment(&mut new_ast, sketch, segment_id)
1256                .map_err(KclErrorWithOutputs::no_outputs)?;
1257        }
1258
1259        self.execute_after_edit(
1260            ctx,
1261            sketch,
1262            sketch_block_ref,
1263            &mut new_ast,
1264            ExecuteAfterEditOptions {
1265                segment_ids_edited: Default::default(),
1266                edit_kind: EditDeleteKind::DeleteNonSketch,
1267                commit_solved_initial_guesses: true,
1268            },
1269        )
1270        .await
1271    }
1272
1273    async fn add_constraint(
1274        &mut self,
1275        ctx: &ExecutorContext,
1276        _version: Version,
1277        sketch: ObjectId,
1278        constraint: Constraint,
1279    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1280        // TODO: Check version.
1281
1282        // Save the original state as a backup - we'll restore it if anything fails
1283        let original_program = self.program.clone();
1284        let original_scene_graph = self.scene_graph.clone();
1285
1286        let mut new_ast = self.program.ast.clone();
1287        let sketch_block_ref = match constraint {
1288            Constraint::Coincident(coincident) => self
1289                .add_coincident(sketch, coincident, &mut new_ast)
1290                .await
1291                .map_err(KclErrorWithOutputs::no_outputs)?,
1292            Constraint::Distance(distance) => self
1293                .add_distance(sketch, distance, &mut new_ast)
1294                .await
1295                .map_err(KclErrorWithOutputs::no_outputs)?,
1296            Constraint::EqualRadius(equal_radius) => self
1297                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1298                .await
1299                .map_err(KclErrorWithOutputs::no_outputs)?,
1300            Constraint::Fixed(fixed) => self
1301                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1302                .await
1303                .map_err(KclErrorWithOutputs::no_outputs)?,
1304            Constraint::HorizontalDistance(distance) => self
1305                .add_horizontal_distance(sketch, distance, &mut new_ast)
1306                .await
1307                .map_err(KclErrorWithOutputs::no_outputs)?,
1308            Constraint::VerticalDistance(distance) => self
1309                .add_vertical_distance(sketch, distance, &mut new_ast)
1310                .await
1311                .map_err(KclErrorWithOutputs::no_outputs)?,
1312            Constraint::Horizontal(horizontal) => self
1313                .add_horizontal(sketch, horizontal, &mut new_ast)
1314                .await
1315                .map_err(KclErrorWithOutputs::no_outputs)?,
1316            Constraint::LinesEqualLength(lines_equal_length) => self
1317                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1318                .await
1319                .map_err(KclErrorWithOutputs::no_outputs)?,
1320            Constraint::Midpoint(midpoint) => self
1321                .add_midpoint(sketch, midpoint, &mut new_ast)
1322                .await
1323                .map_err(KclErrorWithOutputs::no_outputs)?,
1324            Constraint::Parallel(parallel) => self
1325                .add_parallel(sketch, parallel, &mut new_ast)
1326                .await
1327                .map_err(KclErrorWithOutputs::no_outputs)?,
1328            Constraint::Perpendicular(perpendicular) => self
1329                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1330                .await
1331                .map_err(KclErrorWithOutputs::no_outputs)?,
1332            Constraint::Radius(radius) => self
1333                .add_radius(sketch, radius, &mut new_ast)
1334                .await
1335                .map_err(KclErrorWithOutputs::no_outputs)?,
1336            Constraint::Diameter(diameter) => self
1337                .add_diameter(sketch, diameter, &mut new_ast)
1338                .await
1339                .map_err(KclErrorWithOutputs::no_outputs)?,
1340            Constraint::Symmetric(symmetric) => self
1341                .add_symmetric(sketch, symmetric, &mut new_ast)
1342                .await
1343                .map_err(KclErrorWithOutputs::no_outputs)?,
1344            Constraint::Vertical(vertical) => self
1345                .add_vertical(sketch, vertical, &mut new_ast)
1346                .await
1347                .map_err(KclErrorWithOutputs::no_outputs)?,
1348            Constraint::Angle(lines_at_angle) => self
1349                .add_angle(sketch, lines_at_angle, &mut new_ast)
1350                .await
1351                .map_err(KclErrorWithOutputs::no_outputs)?,
1352            Constraint::Tangent(tangent) => self
1353                .add_tangent(sketch, tangent, &mut new_ast)
1354                .await
1355                .map_err(KclErrorWithOutputs::no_outputs)?,
1356        };
1357
1358        let result = self
1359            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1360            .await;
1361
1362        // If execution failed, restore the original state to prevent corruption
1363        if result.is_err() {
1364            self.program = original_program;
1365            self.scene_graph = original_scene_graph;
1366        }
1367
1368        result
1369    }
1370
1371    async fn chain_segment(
1372        &mut self,
1373        ctx: &ExecutorContext,
1374        version: Version,
1375        sketch: ObjectId,
1376        previous_segment_end_point_id: ObjectId,
1377        segment: SegmentCtor,
1378        _label: Option<String>,
1379    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1380        // TODO: Check version.
1381
1382        // First, add the segment (line) to get its start point ID
1383        let SegmentCtor::Line(line_ctor) = segment else {
1384            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1385                "chain_segment currently only supports Line segments, got {}",
1386                segment.human_friendly_kind_with_article(),
1387            ))));
1388        };
1389
1390        // Add the line segment first - this updates self.program and self.scene_graph
1391        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1392
1393        // Find the new line's start point ID from the updated scene graph
1394        // add_line updates self.scene_graph, so we can use that
1395        let new_line_id = first_scene_delta
1396            .new_objects
1397            .iter()
1398            .find(|&obj_id| {
1399                let obj = self.scene_graph.objects.get(obj_id.0);
1400                if let Some(obj) = obj {
1401                    matches!(
1402                        &obj.kind,
1403                        ObjectKind::Segment {
1404                            segment: Segment::Line(_)
1405                        }
1406                    )
1407                } else {
1408                    false
1409                }
1410            })
1411            .ok_or_else(|| {
1412                KclErrorWithOutputs::no_outputs(KclError::refactor(
1413                    "Failed to find new line segment in scene graph".to_string(),
1414                ))
1415            })?;
1416
1417        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1418            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1419                "New line object not found: {new_line_id:?}"
1420            )))
1421        })?;
1422
1423        let ObjectKind::Segment {
1424            segment: new_line_segment,
1425        } = &new_line_obj.kind
1426        else {
1427            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1428                "Object is not a segment: {new_line_obj:?}"
1429            ))));
1430        };
1431
1432        let Segment::Line(new_line) = new_line_segment else {
1433            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1434                "Segment is not a line: {new_line_segment:?}"
1435            ))));
1436        };
1437
1438        let new_line_start_point_id = new_line.start;
1439
1440        // Now add the coincident constraint between the previous end point and the new line's start point.
1441        let coincident = Coincident {
1442            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1443        };
1444
1445        let (final_src_delta, final_scene_delta) = self
1446            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1447            .await?;
1448
1449        // Combine new objects from the line addition and the constraint addition.
1450        // Both add_line and add_constraint now populate new_objects correctly.
1451        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1452        combined_new_objects.extend(final_scene_delta.new_objects);
1453
1454        let scene_graph_delta = SceneGraphDelta {
1455            new_graph: self.scene_graph_for_ui(),
1456            invalidates_ids: false,
1457            new_objects: combined_new_objects,
1458            exec_outcome: final_scene_delta.exec_outcome,
1459        };
1460
1461        Ok((final_src_delta, scene_graph_delta))
1462    }
1463
1464    async fn edit_constraint(
1465        &mut self,
1466        ctx: &ExecutorContext,
1467        _version: Version,
1468        sketch: ObjectId,
1469        constraint_id: ObjectId,
1470        value_expression: String,
1471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1472        // TODO: Check version.
1473        let sketch_block_ref =
1474            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1475
1476        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1477            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1478        })?;
1479        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1480            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1481                "Object is not a constraint: {constraint_id:?}"
1482            ))));
1483        }
1484
1485        let mut new_ast = self.program.ast.clone();
1486
1487        // Parse the expression string into an AST node.
1488        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1489            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1490                "Invalid constraint value",
1491                &e,
1492            )))
1493        })?;
1494        if !errors.is_empty() {
1495            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1496                format_compilation_issues("Invalid constraint value", &errors),
1497            )));
1498        }
1499        let mut parsed = parsed.ok_or_else(|| {
1500            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1501        })?;
1502        if parsed.ast.body.is_empty() {
1503            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1504                "Empty value expression".to_string(),
1505            )));
1506        }
1507        let first = parsed.ast.body.remove(0);
1508        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1509            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1510                "Value expression must be a simple expression".to_string(),
1511            )));
1512        };
1513
1514        let new_value: ast::BinaryPart = expr_stmt
1515            .inner
1516            .expression
1517            .try_into()
1518            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1519
1520        self.mutate_ast(
1521            &mut new_ast,
1522            constraint_id,
1523            AstMutateCommand::EditConstraintValue { value: new_value },
1524        )
1525        .map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527        self.execute_after_edit(
1528            ctx,
1529            sketch,
1530            sketch_block_ref,
1531            &mut new_ast,
1532            ExecuteAfterEditOptions {
1533                segment_ids_edited: Default::default(),
1534                edit_kind: EditDeleteKind::Edit,
1535                commit_solved_initial_guesses: true,
1536            },
1537        )
1538        .await
1539    }
1540
1541    async fn edit_distance_constraint_label_position(
1542        &mut self,
1543        ctx: &ExecutorContext,
1544        _version: Version,
1545        sketch: ObjectId,
1546        constraint_id: ObjectId,
1547        label_position: Point2d<Number>,
1548        anchor_segment_ids: Vec<ObjectId>,
1549    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1550        // TODO: Check version.
1551        let sketch_block_ref =
1552            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1553
1554        let mut new_ast = self.program.ast.clone();
1555        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1556            .map_err(KclErrorWithOutputs::no_outputs)?;
1557        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1558
1559        self.execute_after_edit(
1560            ctx,
1561            sketch,
1562            sketch_block_ref,
1563            &mut new_ast,
1564            ExecuteAfterEditOptions {
1565                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1566                edit_kind: EditDeleteKind::Edit,
1567                commit_solved_initial_guesses,
1568            },
1569        )
1570        .await
1571    }
1572
1573    /// Splitting a segment means creating a new segment, editing the old one, and then
1574    /// migrating a bunch of the constraints from the original segment to the new one
1575    /// (i.e. deleting them and re-adding them on the other segment).
1576    ///
1577    /// To keep this efficient we require as few executions as possible: we create the
1578    /// new segment first (to get its id), then do all edits and new constraints, and
1579    /// do all deletes at the end (since deletes invalidate ids).
1580    async fn batch_split_segment_operations(
1581        &mut self,
1582        ctx: &ExecutorContext,
1583        _version: Version,
1584        sketch: ObjectId,
1585        edit_segments: Vec<ExistingSegmentCtor>,
1586        add_constraints: Vec<Constraint>,
1587        delete_constraint_ids: Vec<ObjectId>,
1588        _new_segment_info: sketch::NewSegmentInfo,
1589    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1590        // TODO: Check version.
1591        let sketch_block_ref =
1592            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1593
1594        let mut new_ast = self.program.ast.clone();
1595        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1596
1597        // Step 1: Edit segments
1598        for segment in edit_segments {
1599            segment_ids_edited.insert(segment.id);
1600            match segment.ctor {
1601                SegmentCtor::Point(ctor) => self
1602                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1603                    .map_err(KclErrorWithOutputs::no_outputs)?,
1604                SegmentCtor::Line(ctor) => self
1605                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1606                    .map_err(KclErrorWithOutputs::no_outputs)?,
1607                SegmentCtor::Arc(ctor) => self
1608                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1609                    .map_err(KclErrorWithOutputs::no_outputs)?,
1610                SegmentCtor::Circle(ctor) => self
1611                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1612                    .map_err(KclErrorWithOutputs::no_outputs)?,
1613                SegmentCtor::ControlPointSpline(ctor) => self
1614                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1615                    .map_err(KclErrorWithOutputs::no_outputs)?,
1616            }
1617        }
1618
1619        // Step 2: Add all constraints
1620        for constraint in add_constraints {
1621            match constraint {
1622                Constraint::Coincident(coincident) => {
1623                    self.add_coincident(sketch, coincident, &mut new_ast)
1624                        .await
1625                        .map_err(KclErrorWithOutputs::no_outputs)?;
1626                }
1627                Constraint::Distance(distance) => {
1628                    self.add_distance(sketch, distance, &mut new_ast)
1629                        .await
1630                        .map_err(KclErrorWithOutputs::no_outputs)?;
1631                }
1632                Constraint::EqualRadius(equal_radius) => {
1633                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1634                        .await
1635                        .map_err(KclErrorWithOutputs::no_outputs)?;
1636                }
1637                Constraint::Fixed(fixed) => {
1638                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1639                        .await
1640                        .map_err(KclErrorWithOutputs::no_outputs)?;
1641                }
1642                Constraint::HorizontalDistance(distance) => {
1643                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1644                        .await
1645                        .map_err(KclErrorWithOutputs::no_outputs)?;
1646                }
1647                Constraint::VerticalDistance(distance) => {
1648                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1649                        .await
1650                        .map_err(KclErrorWithOutputs::no_outputs)?;
1651                }
1652                Constraint::Horizontal(horizontal) => {
1653                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1654                        .await
1655                        .map_err(KclErrorWithOutputs::no_outputs)?;
1656                }
1657                Constraint::LinesEqualLength(lines_equal_length) => {
1658                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1659                        .await
1660                        .map_err(KclErrorWithOutputs::no_outputs)?;
1661                }
1662                Constraint::Midpoint(midpoint) => {
1663                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1664                        .await
1665                        .map_err(KclErrorWithOutputs::no_outputs)?;
1666                }
1667                Constraint::Parallel(parallel) => {
1668                    self.add_parallel(sketch, parallel, &mut new_ast)
1669                        .await
1670                        .map_err(KclErrorWithOutputs::no_outputs)?;
1671                }
1672                Constraint::Perpendicular(perpendicular) => {
1673                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1674                        .await
1675                        .map_err(KclErrorWithOutputs::no_outputs)?;
1676                }
1677                Constraint::Vertical(vertical) => {
1678                    self.add_vertical(sketch, vertical, &mut new_ast)
1679                        .await
1680                        .map_err(KclErrorWithOutputs::no_outputs)?;
1681                }
1682                Constraint::Diameter(diameter) => {
1683                    self.add_diameter(sketch, diameter, &mut new_ast)
1684                        .await
1685                        .map_err(KclErrorWithOutputs::no_outputs)?;
1686                }
1687                Constraint::Radius(radius) => {
1688                    self.add_radius(sketch, radius, &mut new_ast)
1689                        .await
1690                        .map_err(KclErrorWithOutputs::no_outputs)?;
1691                }
1692                Constraint::Symmetric(symmetric) => {
1693                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1694                        .await
1695                        .map_err(KclErrorWithOutputs::no_outputs)?;
1696                }
1697                Constraint::Angle(angle) => {
1698                    self.add_angle(sketch, angle, &mut new_ast)
1699                        .await
1700                        .map_err(KclErrorWithOutputs::no_outputs)?;
1701                }
1702                Constraint::Tangent(tangent) => {
1703                    self.add_tangent(sketch, tangent, &mut new_ast)
1704                        .await
1705                        .map_err(KclErrorWithOutputs::no_outputs)?;
1706                }
1707            }
1708        }
1709
1710        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1711        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1712
1713        let has_constraint_deletions = !constraint_ids_set.is_empty();
1714        for constraint_id in constraint_ids_set {
1715            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1716                .map_err(KclErrorWithOutputs::no_outputs)?;
1717        }
1718
1719        // Step 4: Execute once at the end
1720        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1721        // But we'll manually set invalidates_ids: true if we deleted constraints
1722        let (source_delta, mut scene_graph_delta) = self
1723            .execute_after_edit(
1724                ctx,
1725                sketch,
1726                sketch_block_ref,
1727                &mut new_ast,
1728                ExecuteAfterEditOptions {
1729                    segment_ids_edited,
1730                    edit_kind: EditDeleteKind::Edit,
1731                    commit_solved_initial_guesses: true,
1732                },
1733            )
1734            .await?;
1735
1736        // If we deleted constraints, set invalidates_ids: true
1737        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1738        if has_constraint_deletions {
1739            scene_graph_delta.invalidates_ids = true;
1740        }
1741
1742        Ok((source_delta, scene_graph_delta))
1743    }
1744
1745    async fn batch_tail_cut_operations(
1746        &mut self,
1747        ctx: &ExecutorContext,
1748        _version: Version,
1749        sketch: ObjectId,
1750        edit_segments: Vec<ExistingSegmentCtor>,
1751        add_constraints: Vec<Constraint>,
1752        delete_constraint_ids: Vec<ObjectId>,
1753        additional_edited_segment_ids: Vec<ObjectId>,
1754    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1755        let sketch_block_ref =
1756            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1757
1758        let mut new_ast = self.program.ast.clone();
1759        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1760
1761        // Step 1: Edit segments (usually a single segment for tail cut)
1762        for segment in edit_segments {
1763            segment_ids_edited.insert(segment.id);
1764            match segment.ctor {
1765                SegmentCtor::Point(ctor) => self
1766                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1767                    .map_err(KclErrorWithOutputs::no_outputs)?,
1768                SegmentCtor::Line(ctor) => self
1769                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1770                    .map_err(KclErrorWithOutputs::no_outputs)?,
1771                SegmentCtor::Arc(ctor) => self
1772                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1773                    .map_err(KclErrorWithOutputs::no_outputs)?,
1774                SegmentCtor::Circle(ctor) => self
1775                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1776                    .map_err(KclErrorWithOutputs::no_outputs)?,
1777                SegmentCtor::ControlPointSpline(ctor) => self
1778                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1779                    .map_err(KclErrorWithOutputs::no_outputs)?,
1780            }
1781        }
1782
1783        segment_ids_edited.extend(additional_edited_segment_ids);
1784
1785        // Step 2: Add coincident constraints
1786        for constraint in add_constraints {
1787            match constraint {
1788                Constraint::Coincident(coincident) => {
1789                    self.add_coincident(sketch, coincident, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                other => {
1794                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1795                        "unsupported constraint in tail cut batch: {other:?}"
1796                    ))));
1797                }
1798            }
1799        }
1800
1801        // Step 3: Delete constraints (if any)
1802        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1803
1804        let has_constraint_deletions = !constraint_ids_set.is_empty();
1805        for constraint_id in constraint_ids_set {
1806            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1807                .map_err(KclErrorWithOutputs::no_outputs)?;
1808        }
1809
1810        // Step 4: Single execute_after_edit
1811        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1812        // But we'll manually set invalidates_ids: true if we deleted constraints
1813        let (source_delta, mut scene_graph_delta) = self
1814            .execute_after_edit(
1815                ctx,
1816                sketch,
1817                sketch_block_ref,
1818                &mut new_ast,
1819                ExecuteAfterEditOptions {
1820                    segment_ids_edited,
1821                    edit_kind: EditDeleteKind::Edit,
1822                    commit_solved_initial_guesses: true,
1823                },
1824            )
1825            .await?;
1826
1827        // If we deleted constraints, set invalidates_ids: true
1828        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1829        if has_constraint_deletions {
1830            scene_graph_delta.invalidates_ids = true;
1831        }
1832
1833        Ok((source_delta, scene_graph_delta))
1834    }
1835}
1836
1837impl FrontendState {
1838    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1839        self.program = program.clone();
1840
1841        // Execute so that the objects are updated and available for the next
1842        // API call.
1843        // This always uses engine execution (not mock) so that things are cached.
1844        // Engine execution now runs freedom analysis automatically.
1845        // Keep existing checkpoints alive here. History may still reference
1846        // older committed sketch states across a direct-edit boundary, and a
1847        // checkpoint restore is a full state replacement anyway. We append a
1848        // fresh baseline checkpoint after the full execution below.
1849        // Clear the freedom cache since IDs might have changed after direct editing
1850        // and we're about to run freedom analysis which will repopulate it.
1851        self.point_freedom_cache.clear();
1852        match ctx.run_with_caching(program).await {
1853            Ok(outcome) => {
1854                let outcome = self.update_state_after_exec(outcome, true);
1855                let checkpoint_id = self
1856                    .create_sketch_checkpoint(outcome.clone())
1857                    .await
1858                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1859                Ok(SetProgramOutcome::Success {
1860                    scene_graph: Box::new(self.scene_graph_for_ui()),
1861                    exec_outcome: Box::new(outcome),
1862                    checkpoint_id: Some(checkpoint_id),
1863                })
1864            }
1865            Err(mut err) => {
1866                // Don't return an error just because execution failed. Instead,
1867                // update state as much as possible.
1868                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1869                self.update_state_after_exec(outcome, true);
1870                err.scene_graph = Some(self.scene_graph_for_ui());
1871                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1872            }
1873        }
1874    }
1875
1876    /// Decorate engine execution such that our state is updated and the scene
1877    /// graph is added to the return.
1878    pub async fn engine_execute(
1879        &mut self,
1880        ctx: &ExecutorContext,
1881        program: Program,
1882    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1883        self.program = program.clone();
1884
1885        // Engine execution now runs freedom analysis automatically. Clear the
1886        // freedom cache since IDs might have changed after direct editing, and
1887        // we're about to run freedom analysis which will repopulate it.
1888        self.point_freedom_cache.clear();
1889        match ctx.run_with_caching(program).await {
1890            Ok(outcome) => {
1891                let outcome = self.update_state_after_exec(outcome, true);
1892                Ok(SceneGraphDelta {
1893                    new_graph: self.scene_graph_for_ui(),
1894                    exec_outcome: outcome,
1895                    // We don't know what the new objects are.
1896                    new_objects: Default::default(),
1897                    // We don't know if IDs were invalidated.
1898                    invalidates_ids: Default::default(),
1899                })
1900            }
1901            Err(mut err) => {
1902                // Update state as much as possible, even when there's an error.
1903                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1904                self.update_state_after_exec(outcome, true);
1905                err.scene_graph = Some(self.scene_graph_for_ui());
1906                Err(err)
1907            }
1908        }
1909    }
1910
1911    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1912        if matches!(err.error, KclError::EngineHangup { .. }) {
1913            // It's not ideal to special-case this, but this error is very
1914            // common during development, and it causes confusing downstream
1915            // errors that have nothing to do with the actual problem.
1916            return Err(err);
1917        }
1918
1919        let KclErrorWithOutputs {
1920            error,
1921            mut non_fatal,
1922            variables,
1923            operations,
1924            artifact_graph,
1925            scene_objects,
1926            source_range_to_object,
1927            var_solutions,
1928            refactor_metadata,
1929            filenames,
1930            default_planes,
1931            ..
1932        } = err;
1933
1934        if let Some(source_range) = error.source_ranges().first() {
1935            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1936        } else {
1937            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1938        }
1939
1940        Ok(ExecOutcome {
1941            variables,
1942            filenames,
1943            operations,
1944            artifact_graph,
1945            scene_objects,
1946            source_range_to_object,
1947            var_solutions,
1948            refactor_metadata,
1949            issues: non_fatal,
1950            default_planes,
1951        })
1952    }
1953
1954    async fn add_point(
1955        &mut self,
1956        ctx: &ExecutorContext,
1957        sketch: ObjectId,
1958        ctor: PointCtor,
1959    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1960        // Create updated KCL source from args.
1961        let at_ast = to_ast_point2d(&ctor.position)
1962            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1963        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1964            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1965            unlabeled: None,
1966            arguments: vec![ast::LabeledArg {
1967                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1968                arg: at_ast,
1969            }],
1970            digest: None,
1971            non_code_meta: Default::default(),
1972        })));
1973
1974        // Look up existing sketch.
1975        let sketch_id = sketch;
1976        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1977            #[cfg(target_arch = "wasm32")]
1978            web_sys::console::error_1(
1979                &format!(
1980                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1981                    self.scene_graph.objects
1982                )
1983                .into(),
1984            );
1985            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1986        })?;
1987        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1988            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1989                "Object is not a sketch, it is {}",
1990                sketch_object.kind.human_friendly_kind_with_article(),
1991            ))));
1992        };
1993        // Add the point to the AST of the sketch block.
1994        let mut new_ast = self.program.ast.clone();
1995        let (sketch_block_ref, _) = self
1996            .mutate_ast(
1997                &mut new_ast,
1998                sketch_id,
1999                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2000            )
2001            .map_err(KclErrorWithOutputs::no_outputs)?;
2002        // Convert to string source to create real source ranges.
2003        let new_source = source_from_ast(&new_ast);
2004        // Parse the new KCL source.
2005        let new_program = parse_frontend_mutation_source(
2006            &new_source,
2007            "Error parsing KCL source after adding point",
2008            "No AST produced after adding point",
2009        )?;
2010
2011        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2012            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2013                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2014            )))
2015        })?;
2016
2017        // Make sure to only set this if there are no errors.
2018        self.program = new_program.clone();
2019
2020        // Truncate after the sketch block for mock execution.
2021        let mut truncated_program = new_program;
2022        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2023            .map_err(KclErrorWithOutputs::no_outputs)?;
2024
2025        // Execute.
2026        let outcome = ctx
2027            .run_mock(
2028                &truncated_program,
2029                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2030            )
2031            .await?;
2032
2033        let new_object_ids = {
2034            let make_err =
2035                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2036            let segment_id = outcome
2037                .source_range_to_object
2038                .get(&point_node_ref.range)
2039                .copied()
2040                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2041            let segment_object = outcome
2042                .scene_objects
2043                .get(segment_id.0)
2044                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2045            let ObjectKind::Segment { segment } = &segment_object.kind else {
2046                return Err(make_err(format!(
2047                    "Object is not a segment, it is {}",
2048                    segment_object.kind.human_friendly_kind_with_article()
2049                )));
2050            };
2051            let Segment::Point(_) = segment else {
2052                return Err(make_err(format!(
2053                    "Segment is not a point, it is {}",
2054                    segment.human_friendly_kind_with_article()
2055                )));
2056            };
2057            vec![segment_id]
2058        };
2059        let src_delta = SourceDelta { text: new_source };
2060        // Uses .no_freedom_analysis() so freedom_analysis: false
2061        let outcome = self.update_state_after_exec(outcome, false);
2062        let scene_graph_delta = SceneGraphDelta {
2063            new_graph: self.scene_graph_for_ui(),
2064            invalidates_ids: false,
2065            new_objects: new_object_ids,
2066            exec_outcome: outcome,
2067        };
2068        Ok((src_delta, scene_graph_delta))
2069    }
2070
2071    async fn add_line(
2072        &mut self,
2073        ctx: &ExecutorContext,
2074        sketch: ObjectId,
2075        ctor: LineCtor,
2076    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2077        // Create updated KCL source from args.
2078        let start_ast = to_ast_point2d(&ctor.start)
2079            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2080        let end_ast = to_ast_point2d(&ctor.end)
2081            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082        let mut arguments = vec![
2083            ast::LabeledArg {
2084                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2085                arg: start_ast,
2086            },
2087            ast::LabeledArg {
2088                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2089                arg: end_ast,
2090            },
2091        ];
2092        // Add construction kwarg if construction is Some(true)
2093        if ctor.construction == Some(true) {
2094            arguments.push(ast::LabeledArg {
2095                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2096                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2097                    value: ast::LiteralValue::Bool(true),
2098                    raw: "true".to_string(),
2099                    digest: None,
2100                }))),
2101            });
2102        }
2103        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2104            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2105            unlabeled: None,
2106            arguments,
2107            digest: None,
2108            non_code_meta: Default::default(),
2109        })));
2110
2111        // Look up existing sketch.
2112        let sketch_id = sketch;
2113        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2114            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2115        })?;
2116        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2117            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2118                "Object is not a sketch, it is {}",
2119                sketch_object.kind.human_friendly_kind_with_article(),
2120            ))));
2121        };
2122        // Add the line to the AST of the sketch block.
2123        let mut new_ast = self.program.ast.clone();
2124        let (sketch_block_ref, _) = self
2125            .mutate_ast(
2126                &mut new_ast,
2127                sketch_id,
2128                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2129            )
2130            .map_err(KclErrorWithOutputs::no_outputs)?;
2131        // Convert to string source to create real source ranges.
2132        let new_source = source_from_ast(&new_ast);
2133        // Parse the new KCL source.
2134        let new_program = parse_frontend_mutation_source(
2135            &new_source,
2136            "Error parsing KCL source after adding line",
2137            "No AST produced after adding line",
2138        )?;
2139
2140        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2141            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2142                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2143            )))
2144        })?;
2145
2146        // Make sure to only set this if there are no errors.
2147        self.program = new_program.clone();
2148
2149        // Truncate after the sketch block for mock execution.
2150        let mut truncated_program = new_program;
2151        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2152            .map_err(KclErrorWithOutputs::no_outputs)?;
2153
2154        // Execute.
2155        let outcome = ctx
2156            .run_mock(
2157                &truncated_program,
2158                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2159            )
2160            .await?;
2161
2162        let new_object_ids = {
2163            let make_err =
2164                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2165            let segment_id = outcome
2166                .source_range_to_object
2167                .get(&line_node_ref.range)
2168                .copied()
2169                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2170            let segment_object = outcome
2171                .scene_object_by_id(segment_id)
2172                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2173            let ObjectKind::Segment { segment } = &segment_object.kind else {
2174                return Err(make_err(format!(
2175                    "Object is not a segment, it is {}",
2176                    segment_object.kind.human_friendly_kind_with_article()
2177                )));
2178            };
2179            let Segment::Line(line) = segment else {
2180                return Err(make_err(format!(
2181                    "Segment is not a line, it is {}",
2182                    segment.human_friendly_kind_with_article()
2183                )));
2184            };
2185            vec![line.start, line.end, segment_id]
2186        };
2187        let src_delta = SourceDelta { text: new_source };
2188        // Uses .no_freedom_analysis() so freedom_analysis: false
2189        let outcome = self.update_state_after_exec(outcome, false);
2190        let scene_graph_delta = SceneGraphDelta {
2191            new_graph: self.scene_graph_for_ui(),
2192            invalidates_ids: false,
2193            new_objects: new_object_ids,
2194            exec_outcome: outcome,
2195        };
2196        Ok((src_delta, scene_graph_delta))
2197    }
2198
2199    async fn add_arc(
2200        &mut self,
2201        ctx: &ExecutorContext,
2202        sketch: ObjectId,
2203        ctor: ArcCtor,
2204    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2205        // Create updated KCL source from args.
2206        let start_ast = to_ast_point2d(&ctor.start)
2207            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208        let end_ast = to_ast_point2d(&ctor.end)
2209            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2210        let center_ast = to_ast_point2d(&ctor.center)
2211            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2212        let mut arguments = vec![
2213            ast::LabeledArg {
2214                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2215                arg: start_ast,
2216            },
2217            ast::LabeledArg {
2218                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2219                arg: end_ast,
2220            },
2221            ast::LabeledArg {
2222                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2223                arg: center_ast,
2224            },
2225        ];
2226        // Add construction kwarg if construction is Some(true)
2227        if ctor.construction == Some(true) {
2228            arguments.push(ast::LabeledArg {
2229                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2230                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2231                    value: ast::LiteralValue::Bool(true),
2232                    raw: "true".to_string(),
2233                    digest: None,
2234                }))),
2235            });
2236        }
2237        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2238            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2239            unlabeled: None,
2240            arguments,
2241            digest: None,
2242            non_code_meta: Default::default(),
2243        })));
2244
2245        // Look up existing sketch.
2246        let sketch_id = sketch;
2247        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2248            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2249        })?;
2250        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2251            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2252                "Object is not a sketch, it is {}",
2253                sketch_object.kind.human_friendly_kind_with_article(),
2254            ))));
2255        };
2256        // Add the arc to the AST of the sketch block.
2257        let mut new_ast = self.program.ast.clone();
2258        let (sketch_block_ref, _) = self
2259            .mutate_ast(
2260                &mut new_ast,
2261                sketch_id,
2262                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2263            )
2264            .map_err(KclErrorWithOutputs::no_outputs)?;
2265        // Convert to string source to create real source ranges.
2266        let new_source = source_from_ast(&new_ast);
2267        // Parse the new KCL source.
2268        let new_program = parse_frontend_mutation_source(
2269            &new_source,
2270            "Error parsing KCL source after adding arc",
2271            "No AST produced after adding arc",
2272        )?;
2273
2274        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2275            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2276                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2277            )))
2278        })?;
2279
2280        // Make sure to only set this if there are no errors.
2281        self.program = new_program.clone();
2282
2283        // Truncate after the sketch block for mock execution.
2284        let mut truncated_program = new_program;
2285        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2286            .map_err(KclErrorWithOutputs::no_outputs)?;
2287
2288        // Execute.
2289        let outcome = ctx
2290            .run_mock(
2291                &truncated_program,
2292                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2293            )
2294            .await?;
2295
2296        let new_object_ids = {
2297            let make_err =
2298                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2299            let segment_id = outcome
2300                .source_range_to_object
2301                .get(&arc_node_ref.range)
2302                .copied()
2303                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2304            let segment_object = outcome
2305                .scene_objects
2306                .get(segment_id.0)
2307                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2308            let ObjectKind::Segment { segment } = &segment_object.kind else {
2309                return Err(make_err(format!(
2310                    "Object is not a segment, it is {}",
2311                    segment_object.kind.human_friendly_kind_with_article()
2312                )));
2313            };
2314            let Segment::Arc(arc) = segment else {
2315                return Err(make_err(format!(
2316                    "Segment is not an arc, it is {}",
2317                    segment.human_friendly_kind_with_article()
2318                )));
2319            };
2320            vec![arc.start, arc.end, arc.center, segment_id]
2321        };
2322        let src_delta = SourceDelta { text: new_source };
2323        // Uses .no_freedom_analysis() so freedom_analysis: false
2324        let outcome = self.update_state_after_exec(outcome, false);
2325        let scene_graph_delta = SceneGraphDelta {
2326            new_graph: self.scene_graph_for_ui(),
2327            invalidates_ids: false,
2328            new_objects: new_object_ids,
2329            exec_outcome: outcome,
2330        };
2331        Ok((src_delta, scene_graph_delta))
2332    }
2333
2334    async fn add_circle(
2335        &mut self,
2336        ctx: &ExecutorContext,
2337        sketch: ObjectId,
2338        ctor: CircleCtor,
2339    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2340        // Create updated KCL source from args.
2341        let start_ast = to_ast_point2d(&ctor.start)
2342            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2343        let center_ast = to_ast_point2d(&ctor.center)
2344            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2345        let mut arguments = vec![
2346            ast::LabeledArg {
2347                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2348                arg: start_ast,
2349            },
2350            ast::LabeledArg {
2351                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2352                arg: center_ast,
2353            },
2354        ];
2355        // Add construction kwarg if construction is Some(true)
2356        if ctor.construction == Some(true) {
2357            arguments.push(ast::LabeledArg {
2358                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2359                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2360                    value: ast::LiteralValue::Bool(true),
2361                    raw: "true".to_string(),
2362                    digest: None,
2363                }))),
2364            });
2365        }
2366        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2367            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2368            unlabeled: None,
2369            arguments,
2370            digest: None,
2371            non_code_meta: Default::default(),
2372        })));
2373
2374        // Look up existing sketch.
2375        let sketch_id = sketch;
2376        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2377            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2378        })?;
2379        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2380            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2381                "Object is not a sketch, it is {}",
2382                sketch_object.kind.human_friendly_kind_with_article(),
2383            ))));
2384        };
2385        // Add the circle to the AST of the sketch block.
2386        let mut new_ast = self.program.ast.clone();
2387        let (sketch_block_ref, _) = self
2388            .mutate_ast(
2389                &mut new_ast,
2390                sketch_id,
2391                AstMutateCommand::AddSketchBlockVarDecl {
2392                    prefix: CIRCLE_VARIABLE.to_owned(),
2393                    expr: circle_ast,
2394                },
2395            )
2396            .map_err(KclErrorWithOutputs::no_outputs)?;
2397        // Convert to string source to create real source ranges.
2398        let new_source = source_from_ast(&new_ast);
2399        // Parse the new KCL source.
2400        let new_program = parse_frontend_mutation_source(
2401            &new_source,
2402            "Error parsing KCL source after adding circle",
2403            "No AST produced after adding circle",
2404        )?;
2405
2406        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409            )))
2410        })?;
2411
2412        // Make sure to only set this if there are no errors.
2413        self.program = new_program.clone();
2414
2415        // Truncate after the sketch block for mock execution.
2416        let mut truncated_program = new_program;
2417        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418            .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420        // Execute.
2421        let outcome = ctx
2422            .run_mock(
2423                &truncated_program,
2424                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425            )
2426            .await?;
2427
2428        let new_object_ids = {
2429            let make_err =
2430                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431            let segment_id = outcome
2432                .source_range_to_object
2433                .get(&circle_node_ref.range)
2434                .copied()
2435                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2436            let segment_object = outcome
2437                .scene_objects
2438                .get(segment_id.0)
2439                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440            let ObjectKind::Segment { segment } = &segment_object.kind else {
2441                return Err(make_err(format!(
2442                    "Object is not a segment, it is {}",
2443                    segment_object.kind.human_friendly_kind_with_article()
2444                )));
2445            };
2446            let Segment::Circle(circle) = segment else {
2447                return Err(make_err(format!(
2448                    "Segment is not a circle, it is {}",
2449                    segment.human_friendly_kind_with_article()
2450                )));
2451            };
2452            vec![circle.start, circle.center, segment_id]
2453        };
2454        let src_delta = SourceDelta { text: new_source };
2455        // Uses .no_freedom_analysis() so freedom_analysis: false
2456        let outcome = self.update_state_after_exec(outcome, false);
2457        let scene_graph_delta = SceneGraphDelta {
2458            new_graph: self.scene_graph_for_ui(),
2459            invalidates_ids: false,
2460            new_objects: new_object_ids,
2461            exec_outcome: outcome,
2462        };
2463        Ok((src_delta, scene_graph_delta))
2464    }
2465
2466    async fn add_control_point_spline(
2467        &mut self,
2468        ctx: &ExecutorContext,
2469        sketch: ObjectId,
2470        ctor: ControlPointSplineCtor,
2471    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2473            .map_err(KclErrorWithOutputs::no_outputs)?;
2474
2475        let points_ast = to_ast_point2d_array(&ctor.points)
2476            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477        let mut arguments = vec![ast::LabeledArg {
2478            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2479            arg: points_ast,
2480        }];
2481        if ctor.construction == Some(true) {
2482            arguments.push(ast::LabeledArg {
2483                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2484                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2485                    value: ast::LiteralValue::Bool(true),
2486                    raw: "true".to_string(),
2487                    digest: None,
2488                }))),
2489            });
2490        }
2491        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2492            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2493            unlabeled: None,
2494            arguments,
2495            digest: None,
2496            non_code_meta: Default::default(),
2497        })));
2498
2499        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2500            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2501        })?;
2502        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2503            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2504                "Object is not a sketch, it is {}",
2505                sketch_object.kind.human_friendly_kind_with_article(),
2506            ))));
2507        };
2508
2509        let mut new_ast = new_program.ast.clone();
2510        let (sketch_block_ref, _) = self
2511            .mutate_ast(
2512                &mut new_ast,
2513                sketch,
2514                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2515            )
2516            .map_err(KclErrorWithOutputs::no_outputs)?;
2517        let new_source = source_from_ast(&new_ast);
2518        let new_program = parse_frontend_mutation_source(
2519            &new_source,
2520            "Error parsing KCL source after adding controlPointSpline",
2521            "No AST produced after adding controlPointSpline",
2522        )?;
2523
2524        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2525            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2526                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2527            )))
2528        })?;
2529
2530        self.program = new_program.clone();
2531
2532        let mut truncated_program = new_program;
2533        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2534            .map_err(KclErrorWithOutputs::no_outputs)?;
2535
2536        let outcome = ctx
2537            .run_mock(
2538                &truncated_program,
2539                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2540            )
2541            .await?;
2542
2543        let new_object_ids = {
2544            let make_err =
2545                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2546            let segment_id = outcome
2547                .source_range_to_object
2548                .get(&spline_node_ref.range)
2549                .copied()
2550                .ok_or_else(|| {
2551                    make_err(format!(
2552                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2553                    ))
2554                })?;
2555            let segment_object = outcome
2556                .scene_objects
2557                .get(segment_id.0)
2558                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2559            let ObjectKind::Segment { segment } = &segment_object.kind else {
2560                return Err(make_err(format!(
2561                    "Object is not a segment, it is {}",
2562                    segment_object.kind.human_friendly_kind_with_article()
2563                )));
2564            };
2565            let Segment::ControlPointSpline(spline) = segment else {
2566                return Err(make_err(format!(
2567                    "Segment is not a control point spline, it is {}",
2568                    segment.human_friendly_kind_with_article()
2569                )));
2570            };
2571
2572            let mut ids = outcome
2573                .scene_objects
2574                .iter()
2575                .filter_map(|obj| match &obj.kind {
2576                    ObjectKind::Segment {
2577                        segment: Segment::Line(line),
2578                    } if line.owner == Some(segment_id) => Some(obj.id),
2579                    _ => None,
2580                })
2581                .collect::<Vec<_>>();
2582            ids.extend(spline.controls.clone());
2583            ids.push(segment_id);
2584            ids
2585        };
2586        let src_delta = SourceDelta { text: new_source };
2587        let outcome = self.update_state_after_exec(outcome, false);
2588        let scene_graph_delta = SceneGraphDelta {
2589            new_graph: self.scene_graph_for_ui(),
2590            invalidates_ids: false,
2591            new_objects: new_object_ids,
2592            exec_outcome: outcome,
2593        };
2594        Ok((src_delta, scene_graph_delta))
2595    }
2596
2597    fn edit_point(
2598        &mut self,
2599        new_ast: &mut ast::Node<ast::Program>,
2600        sketch: ObjectId,
2601        point: ObjectId,
2602        ctor: PointCtor,
2603    ) -> Result<(), KclError> {
2604        // Create updated KCL source from args.
2605        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2606
2607        // Look up existing sketch.
2608        let sketch_id = sketch;
2609        let sketch_object = self
2610            .scene_graph
2611            .objects
2612            .get(sketch_id.0)
2613            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2614        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2615            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2616        };
2617        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2618            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2619        })?;
2620        // Look up existing point.
2621        let point_id = point;
2622        let point_object = self
2623            .scene_graph
2624            .objects
2625            .get(point_id.0)
2626            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2627        let ObjectKind::Segment {
2628            segment: Segment::Point(point),
2629        } = &point_object.kind
2630        else {
2631            return Err(KclError::refactor(format!(
2632                "Object is not a point segment: {point_object:?}"
2633            )));
2634        };
2635
2636        // If the point is part of a line or arc, edit the line/arc instead.
2637        if let Some(owner_id) = point.owner {
2638            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2639                KclError::refactor(format!(
2640                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2641                ))
2642            })?;
2643            let ObjectKind::Segment { segment } = &owner_object.kind else {
2644                return Err(KclError::refactor(format!(
2645                    "Internal: Owner of point is not a segment, but found {}",
2646                    owner_object.kind.human_friendly_kind_with_article()
2647                )));
2648            };
2649
2650            // Handle Line owner
2651            if let Segment::Line(line) = segment {
2652                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2653                    return Err(KclError::refactor(format!(
2654                        "Internal: Owner of point does not have line ctor, but found {}",
2655                        line.ctor.human_friendly_kind_with_article()
2656                    )));
2657                };
2658                let mut line_ctor = line_ctor.clone();
2659                // Which end of the line is this point?
2660                if line.start == point_id {
2661                    line_ctor.start = ctor.position;
2662                } else if line.end == point_id {
2663                    line_ctor.end = ctor.position;
2664                } else {
2665                    return Err(KclError::refactor(format!(
2666                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2667                    )));
2668                }
2669                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2670            }
2671
2672            // Handle Arc owner
2673            if let Segment::Arc(arc) = segment {
2674                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2675                    return Err(KclError::refactor(format!(
2676                        "Internal: Owner of point does not have arc ctor, but found {}",
2677                        arc.ctor.human_friendly_kind_with_article()
2678                    )));
2679                };
2680                let mut arc_ctor = arc_ctor.clone();
2681                // Which point of the arc is this? (center, start, or end)
2682                if arc.center == point_id {
2683                    arc_ctor.center = ctor.position;
2684                } else if arc.start == point_id {
2685                    arc_ctor.start = ctor.position;
2686                } else if arc.end == point_id {
2687                    arc_ctor.end = ctor.position;
2688                } else {
2689                    return Err(KclError::refactor(format!(
2690                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2691                    )));
2692                }
2693                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2694            }
2695
2696            // Handle Circle owner
2697            if let Segment::Circle(circle) = segment {
2698                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2699                    return Err(KclError::refactor(format!(
2700                        "Internal: Owner of point does not have circle ctor, but found {}",
2701                        circle.ctor.human_friendly_kind_with_article()
2702                    )));
2703                };
2704                let mut circle_ctor = circle_ctor.clone();
2705                if circle.center == point_id {
2706                    circle_ctor.center = ctor.position;
2707                } else if circle.start == point_id {
2708                    circle_ctor.start = ctor.position;
2709                } else {
2710                    return Err(KclError::refactor(format!(
2711                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2712                    )));
2713                }
2714                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2715            }
2716
2717            if let Segment::ControlPointSpline(spline) = segment {
2718                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2719                    return Err(KclError::refactor(format!(
2720                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2721                        spline.ctor.human_friendly_kind_with_article()
2722                    )));
2723                };
2724                let mut spline_ctor = spline_ctor.clone();
2725                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2726                    return Err(KclError::refactor(format!(
2727                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2728                    )));
2729                };
2730                spline_ctor.points[control_index] = ctor.position;
2731                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2732            }
2733
2734            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2735            // (fall through to the point editing logic below)
2736        }
2737
2738        // Modify the point AST.
2739        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2740        Ok(())
2741    }
2742
2743    fn edit_line(
2744        &mut self,
2745        new_ast: &mut ast::Node<ast::Program>,
2746        sketch: ObjectId,
2747        line: ObjectId,
2748        ctor: LineCtor,
2749    ) -> Result<(), KclError> {
2750        // Create updated KCL source from args.
2751        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2752        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2753
2754        // Look up existing sketch.
2755        let sketch_id = sketch;
2756        let sketch_object = self
2757            .scene_graph
2758            .objects
2759            .get(sketch_id.0)
2760            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2761        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2762            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2763        };
2764        sketch
2765            .segments
2766            .iter()
2767            .find(|o| **o == line)
2768            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2769        // Look up existing line.
2770        let line_id = line;
2771        let line_object = self
2772            .scene_graph
2773            .objects
2774            .get(line_id.0)
2775            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2776        let ObjectKind::Segment { .. } = &line_object.kind else {
2777            let kind = line_object.kind.human_friendly_kind_with_article();
2778            return Err(KclError::refactor(format!(
2779                "This constraint only works on Segments, but you selected {kind}"
2780            )));
2781        };
2782
2783        // Modify the line AST.
2784        self.mutate_ast(
2785            new_ast,
2786            line_id,
2787            AstMutateCommand::EditLine {
2788                start: new_start_ast,
2789                end: new_end_ast,
2790                construction: ctor.construction,
2791            },
2792        )?;
2793        Ok(())
2794    }
2795
2796    fn edit_arc(
2797        &mut self,
2798        new_ast: &mut ast::Node<ast::Program>,
2799        sketch: ObjectId,
2800        arc: ObjectId,
2801        ctor: ArcCtor,
2802    ) -> Result<(), KclError> {
2803        // Create updated KCL source from args.
2804        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2805        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2806        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2807
2808        // Look up existing sketch.
2809        let sketch_id = sketch;
2810        let sketch_object = self
2811            .scene_graph
2812            .objects
2813            .get(sketch_id.0)
2814            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2815        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2816            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2817        };
2818        sketch
2819            .segments
2820            .iter()
2821            .find(|o| **o == arc)
2822            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2823        // Look up existing arc.
2824        let arc_id = arc;
2825        let arc_object = self
2826            .scene_graph
2827            .objects
2828            .get(arc_id.0)
2829            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2830        let ObjectKind::Segment { .. } = &arc_object.kind else {
2831            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2832        };
2833
2834        // Modify the arc AST.
2835        self.mutate_ast(
2836            new_ast,
2837            arc_id,
2838            AstMutateCommand::EditArc {
2839                start: new_start_ast,
2840                end: new_end_ast,
2841                center: new_center_ast,
2842                construction: ctor.construction,
2843            },
2844        )?;
2845        Ok(())
2846    }
2847
2848    fn edit_circle(
2849        &mut self,
2850        new_ast: &mut ast::Node<ast::Program>,
2851        sketch: ObjectId,
2852        circle: ObjectId,
2853        ctor: CircleCtor,
2854    ) -> Result<(), KclError> {
2855        // Create updated KCL source from args.
2856        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2857        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2858
2859        // Look up existing sketch.
2860        let sketch_id = sketch;
2861        let sketch_object = self
2862            .scene_graph
2863            .objects
2864            .get(sketch_id.0)
2865            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2866        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2867            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2868        };
2869        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2870            KclError::refactor(format!(
2871                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2872            ))
2873        })?;
2874        // Look up existing circle.
2875        let circle_id = circle;
2876        let circle_object = self
2877            .scene_graph
2878            .objects
2879            .get(circle_id.0)
2880            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2881        let ObjectKind::Segment { .. } = &circle_object.kind else {
2882            return Err(KclError::refactor(format!(
2883                "Object is not a segment: {circle_object:?}"
2884            )));
2885        };
2886
2887        // Modify the circle AST.
2888        self.mutate_ast(
2889            new_ast,
2890            circle_id,
2891            AstMutateCommand::EditCircle {
2892                start: new_start_ast,
2893                center: new_center_ast,
2894                construction: ctor.construction,
2895            },
2896        )?;
2897        Ok(())
2898    }
2899
2900    fn edit_control_point_spline(
2901        &mut self,
2902        new_ast: &mut ast::Node<ast::Program>,
2903        sketch: ObjectId,
2904        spline: ObjectId,
2905        ctor: ControlPointSplineCtor,
2906    ) -> Result<(), KclError> {
2907        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2908
2909        let sketch_object = self
2910            .scene_graph
2911            .objects
2912            .get(sketch.0)
2913            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2914        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2915            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2916        };
2917        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2918            KclError::refactor(format!(
2919                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2920            ))
2921        })?;
2922
2923        let spline_object =
2924            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2925                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2926            })?;
2927        let ObjectKind::Segment { .. } = &spline_object.kind else {
2928            return Err(KclError::refactor(format!(
2929                "Object is not a segment: {spline_object:?}"
2930            )));
2931        };
2932
2933        self.mutate_ast(
2934            new_ast,
2935            spline,
2936            AstMutateCommand::EditControlPointSpline {
2937                points: points_ast,
2938                construction: ctor.construction,
2939            },
2940        )?;
2941        Ok(())
2942    }
2943
2944    fn delete_segment(
2945        &mut self,
2946        new_ast: &mut ast::Node<ast::Program>,
2947        sketch: ObjectId,
2948        segment_id: ObjectId,
2949    ) -> Result<(), KclError> {
2950        // Look up existing sketch.
2951        let sketch_id = sketch;
2952        let sketch_object = self
2953            .scene_graph
2954            .objects
2955            .get(sketch_id.0)
2956            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2957        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2958            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2959        };
2960        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2961            KclError::refactor(format!(
2962                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2963            ))
2964        })?;
2965        // Look up existing segment.
2966        let segment_object =
2967            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2968                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2969            })?;
2970        let ObjectKind::Segment { .. } = &segment_object.kind else {
2971            return Err(KclError::refactor(format!(
2972                "Object is not a segment, it is {}",
2973                segment_object.kind.human_friendly_kind_with_article()
2974            )));
2975        };
2976
2977        // Modify the AST to remove the segment.
2978        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2979        Ok(())
2980    }
2981
2982    fn delete_constraint(
2983        &mut self,
2984        new_ast: &mut ast::Node<ast::Program>,
2985        sketch: ObjectId,
2986        constraint_id: ObjectId,
2987    ) -> Result<(), KclError> {
2988        // Look up existing sketch.
2989        let sketch_id = sketch;
2990        let sketch_object = self
2991            .scene_graph
2992            .objects
2993            .get(sketch_id.0)
2994            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2995        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2996            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2997        };
2998        sketch
2999            .constraints
3000            .iter()
3001            .find(|o| **o == constraint_id)
3002            .ok_or_else(|| {
3003                KclError::refactor(format!(
3004                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3005                ))
3006            })?;
3007        // Look up existing constraint.
3008        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3009            KclError::refactor(format!(
3010                "Constraint not found in scene graph: constraint={constraint_id:?}"
3011            ))
3012        })?;
3013        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3014            return Err(KclError::refactor(format!(
3015                "Object is not a constraint, it is {}",
3016                constraint_object.kind.human_friendly_kind_with_article()
3017            )));
3018        };
3019
3020        // Modify the AST to remove the constraint.
3021        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3022        Ok(())
3023    }
3024
3025    fn edit_coincident_constraint(
3026        &mut self,
3027        new_ast: &mut ast::Node<ast::Program>,
3028        constraint_id: ObjectId,
3029        segments: Vec<ConstraintSegment>,
3030    ) -> Result<(), KclError> {
3031        if segments.len() < 2 {
3032            return Err(KclError::refactor(format!(
3033                "Coincident constraint must have at least 2 inputs, got {}",
3034                segments.len()
3035            )));
3036        }
3037
3038        let segment_asts = segments
3039            .iter()
3040            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3041            .collect::<Result<Vec<_>, _>>()?;
3042
3043        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3044            elements: segment_asts,
3045            digest: None,
3046            non_code_meta: Default::default(),
3047        })));
3048
3049        self.mutate_ast(
3050            new_ast,
3051            constraint_id,
3052            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3053        )?;
3054        Ok(())
3055    }
3056
3057    fn edit_horizontal_points_constraint(
3058        &mut self,
3059        new_ast: &mut ast::Node<ast::Program>,
3060        constraint_id: ObjectId,
3061        points: Vec<ConstraintSegment>,
3062    ) -> Result<(), KclError> {
3063        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3064    }
3065
3066    fn edit_vertical_points_constraint(
3067        &mut self,
3068        new_ast: &mut ast::Node<ast::Program>,
3069        constraint_id: ObjectId,
3070        points: Vec<ConstraintSegment>,
3071    ) -> Result<(), KclError> {
3072        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3073    }
3074
3075    fn edit_axis_points_constraint(
3076        &mut self,
3077        new_ast: &mut ast::Node<ast::Program>,
3078        constraint_id: ObjectId,
3079        points: Vec<ConstraintSegment>,
3080        constraint_name: &str,
3081    ) -> Result<(), KclError> {
3082        if points.len() < 2 {
3083            return Err(KclError::refactor(format!(
3084                "{constraint_name} points constraint must have at least 2 points, got {}",
3085                points.len()
3086            )));
3087        }
3088
3089        let point_asts = points
3090            .iter()
3091            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3092            .collect::<Result<Vec<_>, _>>()?;
3093
3094        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3095            elements: point_asts,
3096            digest: None,
3097            non_code_meta: Default::default(),
3098        })));
3099
3100        self.mutate_ast(
3101            new_ast,
3102            constraint_id,
3103            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3104        )?;
3105        Ok(())
3106    }
3107
3108    /// updates the equalLength constraint with the given lines
3109    fn edit_equal_length_constraint(
3110        &mut self,
3111        new_ast: &mut ast::Node<ast::Program>,
3112        constraint_id: ObjectId,
3113        lines: Vec<ObjectId>,
3114    ) -> Result<(), KclError> {
3115        if lines.len() < 2 {
3116            return Err(KclError::refactor(format!(
3117                "Lines equal length constraint must have at least 2 lines, got {}",
3118                lines.len()
3119            )));
3120        }
3121
3122        let line_asts = lines
3123            .iter()
3124            .map(|line_id| {
3125                let line_object = self
3126                    .scene_graph
3127                    .objects
3128                    .get(line_id.0)
3129                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3130                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3131                    let kind = line_object.kind.human_friendly_kind_with_article();
3132                    return Err(KclError::refactor(format!(
3133                        "This constraint only works on Segments, but you selected {kind}"
3134                    )));
3135                };
3136                let Segment::Line(_) = line_segment else {
3137                    let kind = line_segment.human_friendly_kind_with_article();
3138                    return Err(KclError::refactor(format!(
3139                        "Only lines can be made equal length, but you selected {kind}"
3140                    )));
3141                };
3142
3143                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3144            })
3145            .collect::<Result<Vec<_>, _>>()?;
3146
3147        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3148            elements: line_asts,
3149            digest: None,
3150            non_code_meta: Default::default(),
3151        })));
3152
3153        self.mutate_ast(
3154            new_ast,
3155            constraint_id,
3156            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3157        )?;
3158        Ok(())
3159    }
3160
3161    /// Updates the parallel constraint with the given lines.
3162    fn edit_parallel_constraint(
3163        &mut self,
3164        new_ast: &mut ast::Node<ast::Program>,
3165        constraint_id: ObjectId,
3166        lines: Vec<ObjectId>,
3167    ) -> Result<(), KclError> {
3168        if lines.len() < 2 {
3169            return Err(KclError::refactor(format!(
3170                "Parallel constraint must have at least 2 lines, got {}",
3171                lines.len()
3172            )));
3173        }
3174
3175        let line_asts = lines
3176            .iter()
3177            .map(|line_id| {
3178                let line_object = self
3179                    .scene_graph
3180                    .objects
3181                    .get(line_id.0)
3182                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3183                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3184                    let kind = line_object.kind.human_friendly_kind_with_article();
3185                    return Err(KclError::refactor(format!(
3186                        "This constraint only works on Segments, but you selected {kind}"
3187                    )));
3188                };
3189                let Segment::Line(_) = line_segment else {
3190                    let kind = line_segment.human_friendly_kind_with_article();
3191                    return Err(KclError::refactor(format!(
3192                        "Only lines can be made parallel, but you selected {kind}"
3193                    )));
3194                };
3195
3196                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3197            })
3198            .collect::<Result<Vec<_>, _>>()?;
3199
3200        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3201            elements: line_asts,
3202            digest: None,
3203            non_code_meta: Default::default(),
3204        })));
3205
3206        self.mutate_ast(
3207            new_ast,
3208            constraint_id,
3209            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3210        )?;
3211        Ok(())
3212    }
3213
3214    /// Updates the equalRadius constraint with the given segments.
3215    fn edit_equal_radius_constraint(
3216        &mut self,
3217        new_ast: &mut ast::Node<ast::Program>,
3218        constraint_id: ObjectId,
3219        input: Vec<ObjectId>,
3220    ) -> Result<(), KclError> {
3221        if input.len() < 2 {
3222            return Err(KclError::refactor(format!(
3223                "equalRadius constraint must have at least 2 segments, got {}",
3224                input.len()
3225            )));
3226        }
3227
3228        let input_asts = input
3229            .iter()
3230            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3231            .collect::<Result<Vec<_>, _>>()?;
3232
3233        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3234            elements: input_asts,
3235            digest: None,
3236            non_code_meta: Default::default(),
3237        })));
3238
3239        self.mutate_ast(
3240            new_ast,
3241            constraint_id,
3242            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3243        )?;
3244        Ok(())
3245    }
3246
3247    async fn execute_after_edit(
3248        &mut self,
3249        ctx: &ExecutorContext,
3250        sketch: ObjectId,
3251        sketch_block_ref: AstNodeRef,
3252        new_ast: &mut ast::Node<ast::Program>,
3253        options: ExecuteAfterEditOptions,
3254    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3255        let ExecuteAfterEditOptions {
3256            segment_ids_edited,
3257            edit_kind,
3258            commit_solved_initial_guesses,
3259        } = options;
3260
3261        // Convert to string source to create real source ranges.
3262        let new_source = source_from_ast(new_ast);
3263        // Parse the new KCL source.
3264        let new_program = parse_frontend_mutation_source(
3265            &new_source,
3266            "Error parsing KCL source after editing",
3267            "No AST produced after editing",
3268        )?;
3269
3270        // Truncate after the sketch block for mock execution.
3271        let is_delete = edit_kind.is_delete();
3272        let truncated_program = {
3273            let mut truncated_program = new_program.clone();
3274            only_sketch_block(
3275                &mut truncated_program.ast,
3276                &sketch_block_ref,
3277                edit_kind.to_change_kind(),
3278            )
3279            .map_err(KclErrorWithOutputs::no_outputs)?;
3280            truncated_program
3281        };
3282
3283        // Execute.
3284        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3285        let mock_config = MockConfig {
3286            sketch_block_id: Some(sketch),
3287            freedom_analysis: is_delete,
3288            segment_ids_edited: segment_ids_edited.clone(),
3289            drag_anchors,
3290            ..Default::default()
3291        };
3292        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3293
3294        // Only now, after execution has succeeded, update self.program.
3295        self.program = new_program;
3296
3297        // Uses freedom_analysis: is_delete
3298        let outcome = self.update_state_after_exec(outcome, is_delete);
3299
3300        let src_delta = if commit_solved_initial_guesses {
3301            self.commit_var_solutions_to_program(&outcome, "editing")?
3302        } else {
3303            SourceDelta { text: new_source }
3304        };
3305        let scene_graph_delta = SceneGraphDelta {
3306            new_graph: self.scene_graph_for_ui(),
3307            invalidates_ids: is_delete,
3308            new_objects: Vec::new(),
3309            exec_outcome: outcome,
3310        };
3311        Ok((src_delta, scene_graph_delta))
3312    }
3313
3314    async fn execute_after_delete_sketch(
3315        &mut self,
3316        ctx: &ExecutorContext,
3317        new_ast: &mut ast::Node<ast::Program>,
3318    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3319        // Convert to string source to create real source ranges.
3320        let new_source = source_from_ast(new_ast);
3321        // Parse the new KCL source.
3322        let new_program = parse_frontend_mutation_source(
3323            &new_source,
3324            "Error parsing KCL source after editing",
3325            "No AST produced after editing",
3326        )?;
3327
3328        // Make sure to only set this if there are no errors.
3329        self.program = new_program.clone();
3330
3331        // We deleted the entire sketch block. It doesn't make sense to truncate
3332        // and execute only the sketch block. We execute the whole program with
3333        // a real engine.
3334
3335        // Execute.
3336        let outcome = ctx.run_with_caching(new_program).await?;
3337        let freedom_analysis_ran = true;
3338
3339        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3340
3341        let src_delta = SourceDelta { text: new_source };
3342        let scene_graph_delta = SceneGraphDelta {
3343            new_graph: self.scene_graph_for_ui(),
3344            invalidates_ids: true,
3345            new_objects: Vec::new(),
3346            exec_outcome: outcome,
3347        };
3348        Ok((src_delta, scene_graph_delta))
3349    }
3350
3351    /// Map a point object id into an AST reference expression for use in
3352    /// constraints. If the point is owned by a segment (line or arc), we
3353    /// reference the appropriate property on that segment (e.g. `line1.start`,
3354    /// `arc1.center`). Otherwise we reference the point directly.
3355    fn point_id_to_ast_reference(
3356        &self,
3357        point_id: ObjectId,
3358        new_ast: &mut ast::Node<ast::Program>,
3359    ) -> Result<ast::Expr, KclError> {
3360        let point_object = self
3361            .scene_graph
3362            .objects
3363            .get(point_id.0)
3364            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3365        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3366            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3367        };
3368        let Segment::Point(point) = point_segment else {
3369            return Err(KclError::refactor(format!(
3370                "Only points are currently supported: {point_object:?}"
3371            )));
3372        };
3373
3374        if let Some(owner_id) = point.owner {
3375            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3376                KclError::refactor(format!(
3377                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3378                ))
3379            })?;
3380            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3381                return Err(KclError::refactor(format!(
3382                    "Owner of point is not a segment, but found {}",
3383                    owner_object.kind.human_friendly_kind_with_article()
3384                )));
3385            };
3386
3387            match owner_segment {
3388                Segment::Line(line) => {
3389                    let property = if line.start == point_id {
3390                        LINE_PROPERTY_START
3391                    } else if line.end == point_id {
3392                        LINE_PROPERTY_END
3393                    } else {
3394                        return Err(KclError::refactor(format!(
3395                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3396                        )));
3397                    };
3398                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3399                }
3400                Segment::Arc(arc) => {
3401                    let property = if arc.start == point_id {
3402                        ARC_PROPERTY_START
3403                    } else if arc.end == point_id {
3404                        ARC_PROPERTY_END
3405                    } else if arc.center == point_id {
3406                        ARC_PROPERTY_CENTER
3407                    } else {
3408                        return Err(KclError::refactor(format!(
3409                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3410                        )));
3411                    };
3412                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3413                }
3414                Segment::Circle(circle) => {
3415                    let property = if circle.start == point_id {
3416                        CIRCLE_PROPERTY_START
3417                    } else if circle.center == point_id {
3418                        CIRCLE_PROPERTY_CENTER
3419                    } else {
3420                        return Err(KclError::refactor(format!(
3421                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3422                        )));
3423                    };
3424                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3425                }
3426                Segment::ControlPointSpline(spline) => {
3427                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3428                        return Err(KclError::refactor(format!(
3429                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3430                        )));
3431                    };
3432                    let owner_expr =
3433                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3434                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3435                    Ok(create_index_expression(controls_expr, index))
3436                }
3437                _ => Err(KclError::refactor(format!(
3438                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3439                ))),
3440            }
3441        } else {
3442            // Standalone point.
3443            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3444        }
3445    }
3446
3447    fn line_id_to_ast_reference(
3448        &self,
3449        line_id: ObjectId,
3450        new_ast: &mut ast::Node<ast::Program>,
3451    ) -> Result<ast::Expr, KclError> {
3452        let line_object = self
3453            .scene_graph
3454            .objects
3455            .get(line_id.0)
3456            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3457        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3458            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3459        };
3460        let Segment::Line(line) = line_segment else {
3461            return Err(KclError::refactor(format!(
3462                "Only lines are currently supported: {line_object:?}"
3463            )));
3464        };
3465
3466        if let Some(owner_id) = line.owner {
3467            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3468                KclError::refactor(format!(
3469                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3470                ))
3471            })?;
3472            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3473                return Err(KclError::refactor(format!(
3474                    "Owner of line is not a segment, but found {}",
3475                    owner_object.kind.human_friendly_kind_with_article()
3476                )));
3477            };
3478
3479            match owner_segment {
3480                Segment::ControlPointSpline(spline) => {
3481                    let Some(index) = spline
3482                        .controls
3483                        .windows(2)
3484                        .position(|window| window[0] == line.start && window[1] == line.end)
3485                    else {
3486                        return Err(KclError::refactor(format!(
3487                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3488                        )));
3489                    };
3490                    let owner_expr =
3491                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3492                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3493                    Ok(create_index_expression(edges_expr, index))
3494                }
3495                _ => Err(KclError::refactor(format!(
3496                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3497                ))),
3498            }
3499        } else {
3500            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3501        }
3502    }
3503
3504    fn coincident_segment_to_ast(
3505        &self,
3506        segment: &ConstraintSegment,
3507        new_ast: &mut ast::Node<ast::Program>,
3508    ) -> Result<ast::Expr, KclError> {
3509        match segment {
3510            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3511            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3512        }
3513    }
3514
3515    fn segment_id_to_constraint_ast_reference(
3516        &self,
3517        segment_id: ObjectId,
3518        new_ast: &mut ast::Node<ast::Program>,
3519    ) -> Result<ast::Expr, KclError> {
3520        let segment_object = self
3521            .scene_graph
3522            .objects
3523            .get(segment_id.0)
3524            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3525        let ObjectKind::Segment { segment } = &segment_object.kind else {
3526            return Err(KclError::refactor(format!(
3527                "Object is not a segment, it is {}",
3528                segment_object.kind.human_friendly_kind_with_article()
3529            )));
3530        };
3531
3532        match segment {
3533            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3534            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3535            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3536            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3537            Segment::ControlPointSpline(_) => {
3538                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3539            }
3540        }
3541    }
3542
3543    fn axis_constraint_segment_to_ast(
3544        &self,
3545        segment: &ConstraintSegment,
3546        new_ast: &mut ast::Node<ast::Program>,
3547    ) -> Result<ast::Expr, KclError> {
3548        match segment {
3549            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3550            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3551        }
3552    }
3553
3554    async fn add_coincident(
3555        &mut self,
3556        sketch: ObjectId,
3557        coincident: Coincident,
3558        new_ast: &mut ast::Node<ast::Program>,
3559    ) -> Result<AstNodeRef, KclError> {
3560        let sketch_id = sketch;
3561        for segment in &coincident.segments {
3562            let ConstraintSegment::Segment(segment_id) = segment else {
3563                continue;
3564            };
3565            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3566                continue;
3567            };
3568            if matches!(
3569                segment_object.kind,
3570                ObjectKind::Segment {
3571                    segment: Segment::ControlPointSpline(_)
3572                }
3573            ) {
3574                return Err(KclError::refactor(
3575                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3576                        .to_owned(),
3577                ));
3578            }
3579        }
3580        let segment_asts = coincident
3581            .segments
3582            .iter()
3583            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3584            .collect::<Result<Vec<_>, _>>()?;
3585        if segment_asts.len() < 2 {
3586            return Err(KclError::refactor(format!(
3587                "Coincident constraint must have at least 2 inputs, got {}",
3588                segment_asts.len()
3589            )));
3590        }
3591
3592        // Create the coincident() call using shared helper.
3593        let coincident_ast = create_coincident_ast(segment_asts);
3594
3595        // Add the line to the AST of the sketch block.
3596        let (sketch_block_ref, _) = self.mutate_ast(
3597            new_ast,
3598            sketch_id,
3599            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3600        )?;
3601        Ok(sketch_block_ref)
3602    }
3603
3604    async fn add_distance(
3605        &mut self,
3606        sketch: ObjectId,
3607        distance: Distance,
3608        new_ast: &mut ast::Node<ast::Program>,
3609    ) -> Result<AstNodeRef, KclError> {
3610        let sketch_id = sketch;
3611        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3612            [pt0, pt1] => [
3613                self.coincident_segment_to_ast(pt0, new_ast)?,
3614                self.coincident_segment_to_ast(pt1, new_ast)?,
3615            ],
3616            _ => {
3617                return Err(KclError::refactor(format!(
3618                    "Distance constraint must have exactly 2 points, got {}",
3619                    distance.points.len()
3620                )));
3621            }
3622        };
3623
3624        let arguments = match &distance.label_position {
3625            Some(label_position) => vec![ast::LabeledArg {
3626                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3627                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3628            }],
3629            None => Default::default(),
3630        };
3631
3632        // Create the distance() call.
3633        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3634            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3635            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3636                ast::ArrayExpression {
3637                    elements: vec![pt0_ast, pt1_ast],
3638                    digest: None,
3639                    non_code_meta: Default::default(),
3640                },
3641            )))),
3642            arguments,
3643            digest: None,
3644            non_code_meta: Default::default(),
3645        })));
3646        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3647            left: distance_call_ast,
3648            operator: ast::BinaryOperator::Eq,
3649            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3650                value: ast::LiteralValue::Number {
3651                    value: distance.distance.value,
3652                    suffix: distance.distance.units,
3653                },
3654                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3655                    KclError::refactor(format!(
3656                        "Could not format numeric suffix: {:?}",
3657                        distance.distance.units
3658                    ))
3659                })?,
3660                digest: None,
3661            }))),
3662            digest: None,
3663        })));
3664
3665        // Add the line to the AST of the sketch block.
3666        let (sketch_block_ref, _) = self.mutate_ast(
3667            new_ast,
3668            sketch_id,
3669            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3670        )?;
3671        Ok(sketch_block_ref)
3672    }
3673
3674    async fn add_angle(
3675        &mut self,
3676        sketch: ObjectId,
3677        angle: Angle,
3678        new_ast: &mut ast::Node<ast::Program>,
3679    ) -> Result<AstNodeRef, KclError> {
3680        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3681            return Err(KclError::refactor(format!(
3682                "Angle constraint must have exactly 2 lines, got {}",
3683                angle.lines.len()
3684            )));
3685        };
3686        let sketch_id = sketch;
3687
3688        // Map the runtime objects back to variable names.
3689        let line0_object = self
3690            .scene_graph
3691            .objects
3692            .get(l0_id.0)
3693            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3694        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3695            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3696        };
3697        let Segment::Line(_) = line0_segment else {
3698            return Err(KclError::refactor(format!(
3699                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3700            )));
3701        };
3702        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3703
3704        let line1_object = self
3705            .scene_graph
3706            .objects
3707            .get(l1_id.0)
3708            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3709        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3710            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3711        };
3712        let Segment::Line(_) = line1_segment else {
3713            return Err(KclError::refactor(format!(
3714                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3715            )));
3716        };
3717        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3718
3719        // Create the angle() call.
3720        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3721            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3722            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3723                ast::ArrayExpression {
3724                    elements: vec![l0_ast, l1_ast],
3725                    digest: None,
3726                    non_code_meta: Default::default(),
3727                },
3728            )))),
3729            arguments: Default::default(),
3730            digest: None,
3731            non_code_meta: Default::default(),
3732        })));
3733        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3734            left: angle_call_ast,
3735            operator: ast::BinaryOperator::Eq,
3736            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3737                value: ast::LiteralValue::Number {
3738                    value: angle.angle.value,
3739                    suffix: angle.angle.units,
3740                },
3741                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3742                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3743                })?,
3744                digest: None,
3745            }))),
3746            digest: None,
3747        })));
3748
3749        // Add the line to the AST of the sketch block.
3750        let (sketch_block_ref, _) = self.mutate_ast(
3751            new_ast,
3752            sketch_id,
3753            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3754        )?;
3755        Ok(sketch_block_ref)
3756    }
3757
3758    async fn add_tangent(
3759        &mut self,
3760        sketch: ObjectId,
3761        tangent: Tangent,
3762        new_ast: &mut ast::Node<ast::Program>,
3763    ) -> Result<AstNodeRef, KclError> {
3764        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3765            return Err(KclError::refactor(format!(
3766                "Tangent constraint must have exactly 2 segments, got {}",
3767                tangent.input.len()
3768            )));
3769        };
3770        let sketch_id = sketch;
3771
3772        let seg0_object = self
3773            .scene_graph
3774            .objects
3775            .get(seg0_id.0)
3776            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3777        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3778            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3779        };
3780        let seg0_ast = match seg0_segment {
3781            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3782                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3783            }
3784            _ => {
3785                return Err(KclError::refactor(format!(
3786                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3787                )));
3788            }
3789        };
3790
3791        let seg1_object = self
3792            .scene_graph
3793            .objects
3794            .get(seg1_id.0)
3795            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3796        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3797            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3798        };
3799        let seg1_ast = match seg1_segment {
3800            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3801                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3802            }
3803            _ => {
3804                return Err(KclError::refactor(format!(
3805                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3806                )));
3807            }
3808        };
3809
3810        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3811        let (sketch_block_ref, _) = self.mutate_ast(
3812            new_ast,
3813            sketch_id,
3814            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3815        )?;
3816        Ok(sketch_block_ref)
3817    }
3818
3819    async fn add_symmetric(
3820        &mut self,
3821        sketch: ObjectId,
3822        symmetric: Symmetric,
3823        new_ast: &mut ast::Node<ast::Program>,
3824    ) -> Result<AstNodeRef, KclError> {
3825        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3826            return Err(KclError::refactor(format!(
3827                "Symmetric constraint must have exactly 2 inputs, got {}",
3828                symmetric.input.len()
3829            )));
3830        };
3831        let sketch_id = sketch;
3832
3833        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3834        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3835        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3836
3837        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3838        let (sketch_block_ref, _) = self.mutate_ast(
3839            new_ast,
3840            sketch_id,
3841            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3842        )?;
3843        Ok(sketch_block_ref)
3844    }
3845
3846    async fn add_midpoint(
3847        &mut self,
3848        sketch: ObjectId,
3849        midpoint: Midpoint,
3850        new_ast: &mut ast::Node<ast::Program>,
3851    ) -> Result<AstNodeRef, KclError> {
3852        let sketch_id = sketch;
3853        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3854
3855        let segment_object = self
3856            .scene_graph
3857            .objects
3858            .get(midpoint.segment.0)
3859            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3860        let ObjectKind::Segment {
3861            segment: midpoint_segment,
3862        } = &segment_object.kind
3863        else {
3864            return Err(KclError::refactor(format!(
3865                "Object must be a segment, but it was {}",
3866                segment_object.kind.human_friendly_kind_with_article()
3867            )));
3868        };
3869        let segment_ast = match midpoint_segment {
3870            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3871            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3872            _ => {
3873                return Err(KclError::refactor(format!(
3874                    "Midpoint target must be a line or arc segment but it was {}",
3875                    midpoint_segment.human_friendly_kind_with_article()
3876                )));
3877            }
3878        };
3879
3880        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3881        let (sketch_block_ref, _) = self.mutate_ast(
3882            new_ast,
3883            sketch_id,
3884            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3885        )?;
3886        Ok(sketch_block_ref)
3887    }
3888
3889    async fn add_equal_radius(
3890        &mut self,
3891        sketch: ObjectId,
3892        equal_radius: EqualRadius,
3893        new_ast: &mut ast::Node<ast::Program>,
3894    ) -> Result<AstNodeRef, KclError> {
3895        if equal_radius.input.len() < 2 {
3896            return Err(KclError::refactor(format!(
3897                "equalRadius constraint must have at least 2 segments, got {}",
3898                equal_radius.input.len()
3899            )));
3900        }
3901
3902        let sketch_id = sketch;
3903        let input_asts = equal_radius
3904            .input
3905            .iter()
3906            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3907            .collect::<Result<Vec<_>, _>>()?;
3908
3909        let equal_radius_ast = create_equal_radius_ast(input_asts);
3910        let (sketch_block_ref, _) = self.mutate_ast(
3911            new_ast,
3912            sketch_id,
3913            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3914        )?;
3915        Ok(sketch_block_ref)
3916    }
3917
3918    async fn add_radius(
3919        &mut self,
3920        sketch: ObjectId,
3921        radius: Radius,
3922        new_ast: &mut ast::Node<ast::Program>,
3923    ) -> Result<AstNodeRef, KclError> {
3924        let params = ArcSizeConstraintParams {
3925            points: vec![radius.arc],
3926            function_name: RADIUS_FN,
3927            value: radius.radius.value,
3928            units: radius.radius.units,
3929            label_position: radius.label_position,
3930            constraint_type_name: "Radius",
3931        };
3932        self.add_arc_size_constraint(sketch, params, new_ast).await
3933    }
3934
3935    async fn add_diameter(
3936        &mut self,
3937        sketch: ObjectId,
3938        diameter: Diameter,
3939        new_ast: &mut ast::Node<ast::Program>,
3940    ) -> Result<AstNodeRef, KclError> {
3941        let params = ArcSizeConstraintParams {
3942            points: vec![diameter.arc],
3943            function_name: DIAMETER_FN,
3944            value: diameter.diameter.value,
3945            units: diameter.diameter.units,
3946            label_position: diameter.label_position,
3947            constraint_type_name: "Diameter",
3948        };
3949        self.add_arc_size_constraint(sketch, params, new_ast).await
3950    }
3951
3952    async fn add_fixed_constraints(
3953        &mut self,
3954        sketch: ObjectId,
3955        points: Vec<FixedPoint>,
3956        new_ast: &mut ast::Node<ast::Program>,
3957    ) -> Result<AstNodeRef, KclError> {
3958        let mut sketch_block_ref = None;
3959
3960        for fixed_point in points {
3961            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3962            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3963                .map_err(|err| KclError::refactor(err.to_string()))?;
3964
3965            let (sketch_ref, _) = self.mutate_ast(
3966                new_ast,
3967                sketch,
3968                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3969            )?;
3970            sketch_block_ref = Some(sketch_ref);
3971        }
3972
3973        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3974    }
3975
3976    async fn add_arc_size_constraint(
3977        &mut self,
3978        sketch: ObjectId,
3979        params: ArcSizeConstraintParams,
3980        new_ast: &mut ast::Node<ast::Program>,
3981    ) -> Result<AstNodeRef, KclError> {
3982        let sketch_id = sketch;
3983
3984        // Constraint must have exactly 1 argument (arc segment)
3985        if params.points.len() != 1 {
3986            return Err(KclError::refactor(format!(
3987                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3988                params.constraint_type_name,
3989                params.points.len()
3990            )));
3991        }
3992
3993        let arc_id = params.points[0];
3994        let arc_object = self
3995            .scene_graph
3996            .objects
3997            .get(arc_id.0)
3998            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3999        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4000            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4001        };
4002        let ref_type = match arc_segment {
4003            Segment::Arc(_) => ARC_VARIABLE,
4004            Segment::Circle(_) => CIRCLE_VARIABLE,
4005            _ => {
4006                return Err(KclError::refactor(format!(
4007                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4008                    params.constraint_type_name
4009                )));
4010            }
4011        };
4012        // Reference the arc/circle segment directly
4013        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4014        let arguments = match &params.label_position {
4015            Some(label_position) => vec![ast::LabeledArg {
4016                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4017                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4018            }],
4019            None => Default::default(),
4020        };
4021
4022        // Create the function call.
4023        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4024            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4025            unlabeled: Some(arc_ast),
4026            arguments,
4027            digest: None,
4028            non_code_meta: Default::default(),
4029        })));
4030        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4031            left: call_ast,
4032            operator: ast::BinaryOperator::Eq,
4033            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4034                value: ast::LiteralValue::Number {
4035                    value: params.value,
4036                    suffix: params.units,
4037                },
4038                raw: format_number_literal(params.value, params.units, None)
4039                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4040                digest: None,
4041            }))),
4042            digest: None,
4043        })));
4044
4045        // Add the line to the AST of the sketch block.
4046        let (sketch_block_ref, _) = self.mutate_ast(
4047            new_ast,
4048            sketch_id,
4049            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4050        )?;
4051        Ok(sketch_block_ref)
4052    }
4053
4054    async fn add_horizontal_distance(
4055        &mut self,
4056        sketch: ObjectId,
4057        distance: Distance,
4058        new_ast: &mut ast::Node<ast::Program>,
4059    ) -> Result<AstNodeRef, KclError> {
4060        let sketch_id = sketch;
4061        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4062            [pt0, pt1] => [
4063                self.coincident_segment_to_ast(pt0, new_ast)?,
4064                self.coincident_segment_to_ast(pt1, new_ast)?,
4065            ],
4066            _ => {
4067                return Err(KclError::refactor(format!(
4068                    "Horizontal distance constraint must have exactly 2 points, got {}",
4069                    distance.points.len()
4070                )));
4071            }
4072        };
4073
4074        let arguments = match &distance.label_position {
4075            Some(label_position) => vec![ast::LabeledArg {
4076                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4077                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4078            }],
4079            None => Default::default(),
4080        };
4081
4082        // Create the horizontalDistance() call.
4083        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4084            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4085            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4086                ast::ArrayExpression {
4087                    elements: vec![pt0_ast, pt1_ast],
4088                    digest: None,
4089                    non_code_meta: Default::default(),
4090                },
4091            )))),
4092            arguments,
4093            digest: None,
4094            non_code_meta: Default::default(),
4095        })));
4096        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4097            left: distance_call_ast,
4098            operator: ast::BinaryOperator::Eq,
4099            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4100                value: ast::LiteralValue::Number {
4101                    value: distance.distance.value,
4102                    suffix: distance.distance.units,
4103                },
4104                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4105                    KclError::refactor(format!(
4106                        "Could not format numeric suffix: {:?}",
4107                        distance.distance.units
4108                    ))
4109                })?,
4110                digest: None,
4111            }))),
4112            digest: None,
4113        })));
4114
4115        // Add the line to the AST of the sketch block.
4116        let (sketch_block_ref, _) = self.mutate_ast(
4117            new_ast,
4118            sketch_id,
4119            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4120        )?;
4121        Ok(sketch_block_ref)
4122    }
4123
4124    async fn add_vertical_distance(
4125        &mut self,
4126        sketch: ObjectId,
4127        distance: Distance,
4128        new_ast: &mut ast::Node<ast::Program>,
4129    ) -> Result<AstNodeRef, KclError> {
4130        let sketch_id = sketch;
4131        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4132            [pt0, pt1] => [
4133                self.coincident_segment_to_ast(pt0, new_ast)?,
4134                self.coincident_segment_to_ast(pt1, new_ast)?,
4135            ],
4136            _ => {
4137                return Err(KclError::refactor(format!(
4138                    "Vertical distance constraint must have exactly 2 points, got {}",
4139                    distance.points.len()
4140                )));
4141            }
4142        };
4143
4144        let arguments = match &distance.label_position {
4145            Some(label_position) => vec![ast::LabeledArg {
4146                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4147                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4148            }],
4149            None => Default::default(),
4150        };
4151
4152        // Create the verticalDistance() call.
4153        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4154            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4155            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4156                ast::ArrayExpression {
4157                    elements: vec![pt0_ast, pt1_ast],
4158                    digest: None,
4159                    non_code_meta: Default::default(),
4160                },
4161            )))),
4162            arguments,
4163            digest: None,
4164            non_code_meta: Default::default(),
4165        })));
4166        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4167            left: distance_call_ast,
4168            operator: ast::BinaryOperator::Eq,
4169            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4170                value: ast::LiteralValue::Number {
4171                    value: distance.distance.value,
4172                    suffix: distance.distance.units,
4173                },
4174                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4175                    KclError::refactor(format!(
4176                        "Could not format numeric suffix: {:?}",
4177                        distance.distance.units
4178                    ))
4179                })?,
4180                digest: None,
4181            }))),
4182            digest: None,
4183        })));
4184
4185        // Add the line to the AST of the sketch block.
4186        let (sketch_block_ref, _) = self.mutate_ast(
4187            new_ast,
4188            sketch_id,
4189            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4190        )?;
4191        Ok(sketch_block_ref)
4192    }
4193
4194    async fn add_horizontal(
4195        &mut self,
4196        sketch: ObjectId,
4197        horizontal: Horizontal,
4198        new_ast: &mut ast::Node<ast::Program>,
4199    ) -> Result<AstNodeRef, KclError> {
4200        let sketch_id = sketch;
4201
4202        // Map the runtime objects back to variable names.
4203        let first_arg_ast = match horizontal {
4204            Horizontal::Line { line } => {
4205                let line_object = self
4206                    .scene_graph
4207                    .objects
4208                    .get(line.0)
4209                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4210                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4211                    let kind = line_object.kind.human_friendly_kind_with_article();
4212                    return Err(KclError::refactor(format!(
4213                        "This constraint only works on Segments, but you selected {kind}"
4214                    )));
4215                };
4216                let Segment::Line(_) = line_segment else {
4217                    return Err(KclError::refactor(format!(
4218                        "Only lines can be made horizontal, but you selected {}",
4219                        line_segment.human_friendly_kind_with_article(),
4220                    )));
4221                };
4222                self.line_id_to_ast_reference(line, new_ast)?
4223            }
4224            Horizontal::Points { points } => {
4225                let point_asts = points
4226                    .iter()
4227                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4228                    .collect::<Result<Vec<_>, _>>()?;
4229                ast::ArrayExpression::new(point_asts).into()
4230            }
4231        };
4232        // Create the horizontal() call using shared helper.
4233        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4234
4235        // Add the line to the AST of the sketch block.
4236        let (sketch_block_ref, _) = self.mutate_ast(
4237            new_ast,
4238            sketch_id,
4239            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4240        )?;
4241        Ok(sketch_block_ref)
4242    }
4243
4244    async fn add_lines_equal_length(
4245        &mut self,
4246        sketch: ObjectId,
4247        lines_equal_length: LinesEqualLength,
4248        new_ast: &mut ast::Node<ast::Program>,
4249    ) -> Result<AstNodeRef, KclError> {
4250        if lines_equal_length.lines.len() < 2 {
4251            return Err(KclError::refactor(format!(
4252                "Lines equal length constraint must have at least 2 lines, got {}",
4253                lines_equal_length.lines.len()
4254            )));
4255        };
4256
4257        let sketch_id = sketch;
4258
4259        // Map the runtime objects back to variable names.
4260        let line_asts = lines_equal_length
4261            .lines
4262            .iter()
4263            .map(|line_id| {
4264                let line_object = self
4265                    .scene_graph
4266                    .objects
4267                    .get(line_id.0)
4268                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4269                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4270                    let kind = line_object.kind.human_friendly_kind_with_article();
4271                    return Err(KclError::refactor(format!(
4272                        "This constraint only works on Segments, but you selected {kind}"
4273                    )));
4274                };
4275                let Segment::Line(_) = line_segment else {
4276                    let kind = line_segment.human_friendly_kind_with_article();
4277                    return Err(KclError::refactor(format!(
4278                        "Only lines can be made equal length, but you selected {kind}"
4279                    )));
4280                };
4281
4282                self.line_id_to_ast_reference(*line_id, new_ast)
4283            })
4284            .collect::<Result<Vec<_>, _>>()?;
4285
4286        // Create the equalLength() call using shared helper.
4287        let equal_length_ast = create_equal_length_ast(line_asts);
4288
4289        // Add the constraint to the AST of the sketch block.
4290        let (sketch_block_ref, _) = self.mutate_ast(
4291            new_ast,
4292            sketch_id,
4293            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4294        )?;
4295        Ok(sketch_block_ref)
4296    }
4297
4298    fn equal_radius_segment_id_to_ast_reference(
4299        &mut self,
4300        segment_id: ObjectId,
4301        new_ast: &mut ast::Node<ast::Program>,
4302    ) -> Result<ast::Expr, KclError> {
4303        let segment_object = self
4304            .scene_graph
4305            .objects
4306            .get(segment_id.0)
4307            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4308        let ObjectKind::Segment { segment } = &segment_object.kind else {
4309            return Err(KclError::refactor(format!(
4310                "Object is not a segment, it was {}",
4311                segment_object.kind.human_friendly_kind_with_article()
4312            )));
4313        };
4314
4315        let ref_type = match segment {
4316            Segment::Arc(_) => ARC_VARIABLE,
4317            Segment::Circle(_) => CIRCLE_VARIABLE,
4318            _ => {
4319                return Err(KclError::refactor(format!(
4320                    "equalRadius supports only arc/circle segments, got {}",
4321                    segment.human_friendly_kind_with_article()
4322                )));
4323            }
4324        };
4325
4326        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4327    }
4328
4329    fn symmetric_input_id_to_ast_reference(
4330        &mut self,
4331        segment_id: ObjectId,
4332        new_ast: &mut ast::Node<ast::Program>,
4333    ) -> Result<ast::Expr, KclError> {
4334        let segment_object = self
4335            .scene_graph
4336            .objects
4337            .get(segment_id.0)
4338            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4339        let ObjectKind::Segment { segment } = &segment_object.kind else {
4340            return Err(KclError::refactor(format!(
4341                "Object is not a segment, it was {}",
4342                segment_object.kind.human_friendly_kind_with_article()
4343            )));
4344        };
4345
4346        match segment {
4347            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4348            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4349            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4350            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4351            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4352                "Symmetric does not yet support control point splines".to_owned(),
4353            )),
4354        }
4355    }
4356
4357    fn symmetric_axis_id_to_ast_reference(
4358        &mut self,
4359        segment_id: ObjectId,
4360        new_ast: &mut ast::Node<ast::Program>,
4361    ) -> Result<ast::Expr, KclError> {
4362        let segment_object = self
4363            .scene_graph
4364            .objects
4365            .get(segment_id.0)
4366            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4367        let ObjectKind::Segment { segment } = &segment_object.kind else {
4368            return Err(KclError::refactor(format!(
4369                "Object is not a segment, it was {}",
4370                segment_object.kind.human_friendly_kind_with_article()
4371            )));
4372        };
4373        match segment {
4374            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4375            _ => Err(KclError::refactor(format!(
4376                "Symmetric axis must be a line, got {}",
4377                segment.human_friendly_kind_with_article()
4378            ))),
4379        }
4380    }
4381
4382    async fn add_parallel(
4383        &mut self,
4384        sketch: ObjectId,
4385        parallel: Parallel,
4386        new_ast: &mut ast::Node<ast::Program>,
4387    ) -> Result<AstNodeRef, KclError> {
4388        if parallel.lines.len() < 2 {
4389            return Err(KclError::refactor(format!(
4390                "Parallel constraint must have at least 2 lines, got {}",
4391                parallel.lines.len()
4392            )));
4393        };
4394
4395        let sketch_id = sketch;
4396
4397        let line_asts = parallel
4398            .lines
4399            .iter()
4400            .map(|line_id| {
4401                let line_object = self
4402                    .scene_graph
4403                    .objects
4404                    .get(line_id.0)
4405                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4406                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4407                    let kind = line_object.kind.human_friendly_kind_with_article();
4408                    return Err(KclError::refactor(format!(
4409                        "This constraint only works on Segments, but you selected {kind}"
4410                    )));
4411                };
4412                let Segment::Line(_) = line_segment else {
4413                    let kind = line_segment.human_friendly_kind_with_article();
4414                    return Err(KclError::refactor(format!(
4415                        "Only lines can be made parallel, but you selected {kind}"
4416                    )));
4417                };
4418
4419                self.line_id_to_ast_reference(*line_id, new_ast)
4420            })
4421            .collect::<Result<Vec<_>, _>>()?;
4422
4423        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4424            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4425            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4426                ast::ArrayExpression {
4427                    elements: line_asts,
4428                    digest: None,
4429                    non_code_meta: Default::default(),
4430                },
4431            )))),
4432            arguments: Default::default(),
4433            digest: None,
4434            non_code_meta: Default::default(),
4435        })));
4436
4437        let (sketch_block_ref, _) = self.mutate_ast(
4438            new_ast,
4439            sketch_id,
4440            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4441        )?;
4442        Ok(sketch_block_ref)
4443    }
4444
4445    async fn add_perpendicular(
4446        &mut self,
4447        sketch: ObjectId,
4448        perpendicular: Perpendicular,
4449        new_ast: &mut ast::Node<ast::Program>,
4450    ) -> Result<AstNodeRef, KclError> {
4451        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4452            .await
4453    }
4454
4455    async fn add_lines_at_angle_constraint(
4456        &mut self,
4457        sketch: ObjectId,
4458        angle_kind: LinesAtAngleKind,
4459        lines: Vec<ObjectId>,
4460        new_ast: &mut ast::Node<ast::Program>,
4461    ) -> Result<AstNodeRef, KclError> {
4462        let &[line0_id, line1_id] = lines.as_slice() else {
4463            return Err(KclError::refactor(format!(
4464                "{} constraint must have exactly 2 lines, got {}",
4465                angle_kind.to_function_name(),
4466                lines.len()
4467            )));
4468        };
4469
4470        let sketch_id = sketch;
4471
4472        // Map the runtime objects back to variable names.
4473        let line0_object = self
4474            .scene_graph
4475            .objects
4476            .get(line0_id.0)
4477            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4478        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4479            let kind = line0_object.kind.human_friendly_kind_with_article();
4480            return Err(KclError::refactor(format!(
4481                "This constraint only works on Segments, but you selected {kind}"
4482            )));
4483        };
4484        let Segment::Line(_) = line0_segment else {
4485            return Err(KclError::refactor(format!(
4486                "Only lines can be made {}, but you selected {}",
4487                angle_kind.to_function_name(),
4488                line0_segment.human_friendly_kind_with_article(),
4489            )));
4490        };
4491        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4492
4493        let line1_object = self
4494            .scene_graph
4495            .objects
4496            .get(line1_id.0)
4497            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4498        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4499            let kind = line1_object.kind.human_friendly_kind_with_article();
4500            return Err(KclError::refactor(format!(
4501                "This constraint only works on Segments, but you selected {kind}"
4502            )));
4503        };
4504        let Segment::Line(_) = line1_segment else {
4505            return Err(KclError::refactor(format!(
4506                "Only lines can be made {}, but you selected {}",
4507                angle_kind.to_function_name(),
4508                line1_segment.human_friendly_kind_with_article(),
4509            )));
4510        };
4511        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4512
4513        // Create the parallel() or perpendicular() call.
4514        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4515            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4516            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4517                ast::ArrayExpression {
4518                    elements: vec![line0_ast, line1_ast],
4519                    digest: None,
4520                    non_code_meta: Default::default(),
4521                },
4522            )))),
4523            arguments: Default::default(),
4524            digest: None,
4525            non_code_meta: Default::default(),
4526        })));
4527
4528        // Add the constraint to the AST of the sketch block.
4529        let (sketch_block_ref, _) = self.mutate_ast(
4530            new_ast,
4531            sketch_id,
4532            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4533        )?;
4534        Ok(sketch_block_ref)
4535    }
4536
4537    async fn add_vertical(
4538        &mut self,
4539        sketch: ObjectId,
4540        vertical: Vertical,
4541        new_ast: &mut ast::Node<ast::Program>,
4542    ) -> Result<AstNodeRef, KclError> {
4543        let sketch_id = sketch;
4544
4545        let first_arg_ast = match vertical {
4546            Vertical::Line { line } => {
4547                // Map the runtime objects back to variable names.
4548                let line_object = self
4549                    .scene_graph
4550                    .objects
4551                    .get(line.0)
4552                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4553                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4554                    let kind = line_object.kind.human_friendly_kind_with_article();
4555                    return Err(KclError::refactor(format!(
4556                        "This constraint only works on Segments, but you selected {kind}"
4557                    )));
4558                };
4559                let Segment::Line(_) = line_segment else {
4560                    return Err(KclError::refactor(format!(
4561                        "Only lines can be made vertical, but you selected {}",
4562                        line_segment.human_friendly_kind_with_article()
4563                    )));
4564                };
4565                self.line_id_to_ast_reference(line, new_ast)?
4566            }
4567            Vertical::Points { points } => {
4568                let point_asts = points
4569                    .iter()
4570                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4571                    .collect::<Result<Vec<_>, _>>()?;
4572                ast::ArrayExpression::new(point_asts).into()
4573            }
4574        };
4575        // Create the vertical() call using shared helper.
4576        let vertical_ast = create_vertical_ast(first_arg_ast);
4577
4578        // Add the line to the AST of the sketch block.
4579        let (sketch_block_ref, _) = self.mutate_ast(
4580            new_ast,
4581            sketch_id,
4582            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4583        )?;
4584        Ok(sketch_block_ref)
4585    }
4586
4587    async fn execute_after_add_constraint(
4588        &mut self,
4589        ctx: &ExecutorContext,
4590        sketch_id: ObjectId,
4591        sketch_block_ref: AstNodeRef,
4592        new_ast: &mut ast::Node<ast::Program>,
4593    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4594        // Convert to string source to create real source ranges.
4595        let new_source = source_from_ast(new_ast);
4596        // Parse the new KCL source.
4597        let new_program = parse_frontend_mutation_source(
4598            &new_source,
4599            "Error parsing KCL source after adding constraint",
4600            "No AST produced after adding constraint",
4601        )?;
4602        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4603            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4604                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4605            )))
4606        })?;
4607
4608        // Truncate after the sketch block for mock execution.
4609        // Use a clone so we don't mutate new_program yet
4610        let mut truncated_program = new_program.clone();
4611        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4612            .map_err(KclErrorWithOutputs::no_outputs)?;
4613
4614        // Execute - if this fails, we haven't modified self yet, so state is safe
4615        let outcome = ctx
4616            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4617            .await?;
4618
4619        let new_object_ids = {
4620            // Extract the constraint ID from the execution outcome using source_range_to_object
4621            let constraint_id = outcome
4622                .source_range_to_object
4623                .get(&constraint_node_ref.range)
4624                .copied()
4625                .ok_or_else(|| {
4626                    KclErrorWithOutputs::from_error_outcome(
4627                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4628                        outcome.clone(),
4629                    )
4630                })?;
4631            vec![constraint_id]
4632        };
4633
4634        // Only now, after all operations succeeded, update self.program.
4635        // This ensures state is only modified if everything succeeds.
4636        self.program = new_program;
4637
4638        // Uses MockConfig::default() which has freedom_analysis: true
4639        let outcome = self.update_state_after_exec(outcome, true);
4640
4641        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4642        let scene_graph_delta = SceneGraphDelta {
4643            new_graph: self.scene_graph_for_ui(),
4644            invalidates_ids: false,
4645            new_objects: new_object_ids,
4646            exec_outcome: outcome,
4647        };
4648        Ok((src_delta, scene_graph_delta))
4649    }
4650
4651    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4652        let commit_failure = || {
4653            KclErrorWithOutputs::from_error_outcome(
4654                KclError::refactor(format!("Could not update KCL after {operation}.")),
4655                outcome.clone(),
4656            )
4657        };
4658
4659        let default_length_unit = self.default_length_unit();
4660        let mut settled_ast = self.program.ast.clone();
4661        let mut committed_solver_value = false;
4662        for (var_range, node_path, value) in &outcome.var_solutions {
4663            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4664                return Err(commit_failure());
4665            };
4666            let new_value = match &lookup {
4667                Some(current_literal) => {
4668                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4669                        continue;
4670                    }
4671                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4672                }
4673                None => {
4674                    // Bare `var` with no initial literal to compare against;
4675                    // always commit, using the module's default length unit as
4676                    // an explicit suffix so the written value carries units.
4677                    Number {
4678                        value: number_value_in_default_length_units(*value, default_length_unit),
4679                        units: default_length_unit.into(),
4680                    }
4681                }
4682            };
4683            committed_solver_value = true;
4684            let source_ref = SourceRef::Simple {
4685                range: *var_range,
4686                node_path: node_path.clone(),
4687            };
4688            mutate_ast_node_by_source_ref(
4689                &mut settled_ast,
4690                &source_ref,
4691                AstMutateCommand::EditVarInitialValue { value: new_value },
4692            )
4693            .map_err(|_| commit_failure())?;
4694        }
4695
4696        if !committed_solver_value {
4697            return Ok(SourceDelta {
4698                text: self.program.original_file_contents.clone(),
4699            });
4700        }
4701
4702        let settled_source = source_from_ast(&settled_ast);
4703        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4704        if !errors.is_empty() {
4705            return Err(commit_failure());
4706        }
4707        let Some(settled_program) = settled_program else {
4708            return Err(commit_failure());
4709        };
4710
4711        self.program = settled_program;
4712
4713        Ok(SourceDelta { text: settled_source })
4714    }
4715
4716    // Find constraints that reference the given segments.
4717    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4718        if segment_ids_set.contains(&segment_id) {
4719            return true;
4720        }
4721
4722        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4723            return false;
4724        };
4725        let ObjectKind::Segment { segment } = &segment_object.kind else {
4726            return false;
4727        };
4728        let Segment::Point(point) = segment else {
4729            return false;
4730        };
4731
4732        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4733    }
4734
4735    fn remaining_constraint_segments(
4736        &self,
4737        segments: &[ConstraintSegment],
4738        segment_ids_set: &AhashIndexSet<ObjectId>,
4739    ) -> Vec<ConstraintSegment> {
4740        segments
4741            .iter()
4742            .copied()
4743            .filter(|segment| match segment {
4744                ConstraintSegment::Origin(_) => true,
4745                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4746            })
4747            .collect()
4748    }
4749
4750    fn find_referenced_constraints(
4751        &self,
4752        sketch_id: ObjectId,
4753        segment_ids_set: &AhashIndexSet<ObjectId>,
4754    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4755        // Look up the sketch.
4756        let sketch_object = self
4757            .scene_graph
4758            .objects
4759            .get(sketch_id.0)
4760            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4761        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4762            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4763        };
4764        let segment_or_owner_matches = |segment_id: ObjectId| {
4765            if segment_ids_set.contains(&segment_id) {
4766                return true;
4767            }
4768            let segment_object = self.scene_graph.objects.get(segment_id.0);
4769            if let Some(obj) = segment_object
4770                && let ObjectKind::Segment { segment } = &obj.kind
4771            {
4772                match segment {
4773                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4774                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4775                    _ => false,
4776                }
4777            } else {
4778                false
4779            }
4780        };
4781        let mut constraint_ids_set = AhashIndexSet::default();
4782        for constraint_id in &sketch.constraints {
4783            let constraint_object = self
4784                .scene_graph
4785                .objects
4786                .get(constraint_id.0)
4787                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4788            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4789                return Err(KclError::refactor(format!(
4790                    "Object is not a constraint, it is {}",
4791                    constraint_object.kind.human_friendly_kind_with_article()
4792                )));
4793            };
4794            let depends_on_segment = match constraint {
4795                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4796                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4797                Constraint::Fixed(fixed) => fixed
4798                    .points
4799                    .iter()
4800                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4801                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4802                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4803                Constraint::EqualRadius(equal_radius) => {
4804                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4805                }
4806                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4807                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4808                Constraint::Horizontal(h) => match h {
4809                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4810                    Horizontal::Points { points } => points.iter().any(|point| match point {
4811                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4812                        ConstraintSegment::Origin(_) => false,
4813                    }),
4814                },
4815                Constraint::Vertical(v) => match v {
4816                    Vertical::Line { line } => segment_or_owner_matches(*line),
4817                    Vertical::Points { points } => points.iter().any(|point| match point {
4818                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4819                        ConstraintSegment::Origin(_) => false,
4820                    }),
4821                },
4822                Constraint::LinesEqualLength(lines_equal_length) => {
4823                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4824                }
4825                Constraint::Midpoint(midpoint) => {
4826                    segment_or_owner_matches(midpoint.segment)
4827                        || matches!(
4828                            midpoint.point,
4829                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4830                        )
4831                }
4832                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4833                Constraint::Perpendicular(perpendicular) => {
4834                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4835                }
4836                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4837                Constraint::Symmetric(symmetric) => {
4838                    segment_or_owner_matches(symmetric.axis)
4839                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4840                }
4841                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4842            };
4843            if depends_on_segment {
4844                constraint_ids_set.insert(*constraint_id);
4845            }
4846        }
4847        Ok(constraint_ids_set)
4848    }
4849
4850    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4851        let mut outcome = outcome;
4852        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4853        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4854
4855        if freedom_analysis_ran {
4856            // When freedom analysis ran, replace the cache entirely with new values
4857            // Don't merge with old values since IDs might have changed
4858            self.point_freedom_cache.clear();
4859            for new_obj in &new_objects {
4860                if let ObjectKind::Segment {
4861                    segment: crate::front::Segment::Point(point),
4862                } = &new_obj.kind
4863                {
4864                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4865                }
4866            }
4867            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4868            // Objects are already correct from the analysis, just use them as-is
4869            self.scene_graph.objects = new_objects;
4870        } else {
4871            // When freedom analysis didn't run, preserve old values and merge
4872            // Before replacing objects, extract and store freedom values from old objects
4873            for old_obj in &self.scene_graph.objects {
4874                if let ObjectKind::Segment {
4875                    segment: crate::front::Segment::Point(point),
4876                } = &old_obj.kind
4877                {
4878                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4879                }
4880            }
4881
4882            // Update objects, preserving stored freedom values when new is Free (might be default)
4883            let mut updated_objects = Vec::with_capacity(new_objects.len());
4884            for new_obj in new_objects {
4885                let mut obj = new_obj;
4886                if let ObjectKind::Segment {
4887                    segment: crate::front::Segment::Point(point),
4888                } = &mut obj.kind
4889                {
4890                    let new_freedom = point.freedom;
4891                    // When freedom_analysis=false, new values are defaults (Free).
4892                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4893                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4894                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4895                    // when there are actually unsatisfied constraints.
4896                    match new_freedom {
4897                        Freedom::Free => {
4898                            match self.point_freedom_cache.get(&obj.id).copied() {
4899                                Some(Freedom::Conflict) => {
4900                                    // Don't preserve Conflict - conflicts are transient
4901                                    // Keep it as Free
4902                                }
4903                                Some(Freedom::Fixed) => {
4904                                    // Preserve Fixed cached value
4905                                    point.freedom = Freedom::Fixed;
4906                                }
4907                                Some(Freedom::Free) => {
4908                                    // If stored is also Free, keep Free (no change needed)
4909                                }
4910                                None => {
4911                                    // If no cached value, keep Free (default)
4912                                }
4913                            }
4914                        }
4915                        Freedom::Fixed => {
4916                            // Use new value (already set)
4917                        }
4918                        Freedom::Conflict => {
4919                            // Use new value (already set)
4920                        }
4921                    }
4922                    // Store the new freedom value (even if it's Free, so we know it was set)
4923                    self.point_freedom_cache.insert(obj.id, point.freedom);
4924                }
4925                updated_objects.push(obj);
4926            }
4927
4928            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4929            self.scene_graph.objects = updated_objects;
4930        }
4931        outcome
4932    }
4933
4934    fn mutate_ast(
4935        &mut self,
4936        ast: &mut ast::Node<ast::Program>,
4937        object_id: ObjectId,
4938        command: AstMutateCommand,
4939    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4940        let sketch_object = self
4941            .scene_graph
4942            .objects
4943            .get(object_id.0)
4944            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4945        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4946    }
4947
4948    fn mutate_constraint_label_position(
4949        &mut self,
4950        ast: &mut ast::Node<ast::Program>,
4951        constraint_id: ObjectId,
4952        label_position: Point2d<Number>,
4953    ) -> Result<(), KclError> {
4954        let object = self
4955            .scene_graph
4956            .objects
4957            .get(constraint_id.0)
4958            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
4959        if !matches!(
4960            &object.kind,
4961            ObjectKind::Constraint {
4962                constraint: Constraint::Distance(_)
4963                    | Constraint::HorizontalDistance(_)
4964                    | Constraint::VerticalDistance(_)
4965                    | Constraint::Radius(_)
4966                    | Constraint::Diameter(_),
4967            }
4968        ) {
4969            return Err(KclError::refactor(format!(
4970                "Object does not support labelPosition: {constraint_id:?}"
4971            )));
4972        }
4973
4974        let label_position = to_ast_point2d_number(&label_position)
4975            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
4976        self.mutate_ast(
4977            ast,
4978            constraint_id,
4979            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
4980        )?;
4981        Ok(())
4982    }
4983}
4984
4985fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4986    // Look up existing sketch.
4987    let sketch_object = scene_graph
4988        .objects
4989        .get(sketch_id.0)
4990        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4991    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4992        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4993    };
4994    expect_single_node_ref(sketch_object)
4995}
4996
4997fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4998    match &object.source {
4999        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5000            range: *range,
5001            node_path: node_path.clone(),
5002        }),
5003        SourceRef::BackTrace { ranges } => {
5004            let [range] = ranges.as_slice() else {
5005                return Err(KclError::refactor(format!(
5006                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5007                    ranges.len()
5008                )));
5009            };
5010            Ok(AstNodeRef {
5011                range: range.0,
5012                node_path: range.1.clone(),
5013            })
5014        }
5015    }
5016}
5017
5018/// This is a deprecated fall-back implementation. Prefer
5019/// [`only_sketch_block()`] to avoid reliance on source ranges.
5020fn only_sketch_block_from_range(
5021    ast: &mut ast::Node<ast::Program>,
5022    sketch_block_range: SourceRange,
5023    edit_kind: ChangeKind,
5024) -> Result<(), KclError> {
5025    let r1 = sketch_block_range;
5026    let matches_range = |r2: SourceRange| -> bool {
5027        // We may have added items to the sketch block, so the end may not be an
5028        // exact match.
5029        match edit_kind {
5030            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5031            // For edit, we don't know whether it grew or shrank.
5032            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5033            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5034            // No edit should be an exact match.
5035            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5036        }
5037    };
5038    let mut found = false;
5039    for item in ast.body.iter_mut() {
5040        match item {
5041            ast::BodyItem::ImportStatement(_) => {}
5042            ast::BodyItem::ExpressionStatement(node) => {
5043                if matches_range(SourceRange::from(&*node))
5044                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5045                {
5046                    sketch_block.is_being_edited = true;
5047                    found = true;
5048                    break;
5049                }
5050            }
5051            ast::BodyItem::VariableDeclaration(node) => {
5052                if matches_range(SourceRange::from(&node.declaration.init))
5053                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5054                {
5055                    sketch_block.is_being_edited = true;
5056                    found = true;
5057                    break;
5058                }
5059            }
5060            ast::BodyItem::TypeDeclaration(_) => {}
5061            ast::BodyItem::ReturnStatement(node) => {
5062                if matches_range(SourceRange::from(&node.argument))
5063                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5064                {
5065                    sketch_block.is_being_edited = true;
5066                    found = true;
5067                    break;
5068                }
5069            }
5070        }
5071    }
5072    if !found {
5073        return Err(KclError::refactor(format!(
5074            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5075        )));
5076    }
5077
5078    Ok(())
5079}
5080
5081fn only_sketch_block(
5082    ast: &mut ast::Node<ast::Program>,
5083    sketch_block_ref: &AstNodeRef,
5084    edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086    let Some(target_node_path) = &sketch_block_ref.node_path else {
5087        #[cfg(target_arch = "wasm32")]
5088        web_sys::console::warn_1(
5089            &format!(
5090                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5091                sketch_block_ref
5092            )
5093            .into(),
5094        );
5095        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5096    };
5097    let mut found = false;
5098    for item in ast.body.iter_mut() {
5099        match item {
5100            ast::BodyItem::ImportStatement(_) => {}
5101            ast::BodyItem::ExpressionStatement(node) => {
5102                // Check the statement.
5103                if let Some(node_path) = &node.node_path
5104                    && node_path == target_node_path
5105                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106                {
5107                    sketch_block.is_being_edited = true;
5108                    found = true;
5109                    break;
5110                }
5111                // Check the expression.
5112                if let Some(node_path) = node.expression.node_path()
5113                    && node_path == target_node_path
5114                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5115                {
5116                    sketch_block.is_being_edited = true;
5117                    found = true;
5118                    break;
5119                }
5120            }
5121            ast::BodyItem::VariableDeclaration(node) => {
5122                if let Some(node_path) = node.declaration.init.node_path()
5123                    && node_path == target_node_path
5124                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5125                {
5126                    sketch_block.is_being_edited = true;
5127                    found = true;
5128                    break;
5129                }
5130            }
5131            ast::BodyItem::TypeDeclaration(_) => {}
5132            ast::BodyItem::ReturnStatement(node) => {
5133                if let Some(node_path) = node.argument.node_path()
5134                    && node_path == target_node_path
5135                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5136                {
5137                    sketch_block.is_being_edited = true;
5138                    found = true;
5139                    break;
5140                }
5141            }
5142        }
5143    }
5144    if !found {
5145        return Err(KclError::refactor(format!(
5146            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5147        )));
5148    }
5149
5150    Ok(())
5151}
5152
5153fn sketch_on_ast_expr(
5154    ast: &mut ast::Node<ast::Program>,
5155    scene_graph: &SceneGraph,
5156    solid_references: &HashMap<Uuid, SolidAstReference>,
5157    on: &Plane,
5158) -> Result<ast::Expr, KclError> {
5159    match on {
5160        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5161        Plane::Object(object_id) => {
5162            let on_object = scene_graph
5163                .objects
5164                .get(object_id.0)
5165                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5166            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5167                return Ok(face_expr);
5168            }
5169            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5170        }
5171        Plane::PrimitiveFace(face) => {
5172            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5173                KclError::refactor(format!(
5174                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5175                    face.solid_id
5176                ))
5177            })?;
5178            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5179            Ok(create_face_of_ast(solid_expr, face_id_expr))
5180        }
5181    }
5182}
5183
5184fn solid_references_from_variables(
5185    ast: &ast::Node<ast::Program>,
5186    variables: &IndexMap<String, KclValueView>,
5187) -> HashMap<Uuid, SolidAstReference> {
5188    let mut references = HashMap::new();
5189
5190    // In-place operations such as shell reuse their input solid's engine ID.
5191    // Later variables overwrite earlier references so mutations target the result.
5192    for item in &ast.body {
5193        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5194            continue;
5195        };
5196        let name = &declaration.declaration.id.name;
5197        let Some(value) = variables.get(name) else {
5198            continue;
5199        };
5200
5201        match value {
5202            KclValueView::Solid { value } => {
5203                references.insert(
5204                    value.id,
5205                    SolidAstReference {
5206                        variable_name: name.clone(),
5207                        output_index: None,
5208                    },
5209                );
5210            }
5211            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5212                for (output_index, entry) in value.iter().enumerate() {
5213                    if let KclValueView::Solid { value } = entry {
5214                        references.insert(
5215                            value.id,
5216                            SolidAstReference {
5217                                variable_name: name.clone(),
5218                                output_index: Some(output_index),
5219                            },
5220                        );
5221                    }
5222                }
5223            }
5224            _ => {}
5225        }
5226    }
5227
5228    references
5229}
5230
5231fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5232    let reference = solid_references.get(&solid_id)?;
5233    let solid_expr = ast_name_expr(reference.variable_name.clone());
5234    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5235}
5236
5237fn sketch_face_of_scene_object_ast_expr(
5238    ast: &mut ast::Node<ast::Program>,
5239    on_object: &crate::front::Object,
5240) -> Result<Option<ast::Expr>, KclError> {
5241    match &on_object.kind {
5242        ObjectKind::Wall(wall) => {
5243            let solid_ref = get_or_insert_ast_reference(
5244                ast,
5245                &source_ref_from_source_ref_range(&wall.source.solid),
5246                "solid",
5247                None,
5248            )?;
5249            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5250                return Err(KclError::refactor(format!(
5251                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5252                    on_object.artifact_id
5253                )));
5254            };
5255            let solid_expr = indexed_solid_expr_for_sweep_output(
5256                ast_name_expr(solid_name_expr.name.name.clone()),
5257                wall.solid_output_index,
5258            );
5259            let sweep_ref = get_or_insert_ast_reference(
5260                ast,
5261                &source_ref_from_source_ref_range(&wall.source.sweep),
5262                "solid",
5263                None,
5264            )?;
5265            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5266                return Err(KclError::refactor(format!(
5267                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5268                    on_object.artifact_id
5269                )));
5270            };
5271            let sweep_name = sweep_name_expr.name.name.clone();
5272            let segment_ref = get_or_insert_ast_reference(
5273                ast,
5274                &source_ref_from_source_ref_range(&wall.source.segment),
5275                LINE_VARIABLE,
5276                None,
5277            )?;
5278
5279            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5280                wall.source
5281                    .path
5282                    .as_ref()
5283                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5284            }) {
5285                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5286                    return Err(KclError::refactor(format!(
5287                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5288                        on_object.artifact_id
5289                    )));
5290                };
5291                create_member_expression(
5292                    create_member_expression(ast_name_expr(region_name), "tags"),
5293                    &segment_name_expr.name.name,
5294                )
5295            } else {
5296                segment_ref
5297            };
5298
5299            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5300        }
5301        ObjectKind::Cap(cap) => {
5302            let solid_ref =
5303                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5304            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5305                return Err(KclError::refactor(format!(
5306                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5307                    on_object.artifact_id
5308                )));
5309            };
5310            let solid_expr = indexed_solid_expr_for_sweep_output(
5311                ast_name_expr(solid_name_expr.name.name.clone()),
5312                cap.solid_output_index,
5313            );
5314            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5315            let face_expr = match cap.kind {
5316                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5317                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5318            };
5319
5320            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5321        }
5322        _ => Ok(None),
5323    }
5324}
5325
5326fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5327    match solid_output_index {
5328        Some(output_index) => create_index_expression(solid_expr, output_index),
5329        None => solid_expr,
5330    }
5331}
5332
5333fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5334    SourceRef::Simple {
5335        range: source.range,
5336        node_path: source.node_path.clone(),
5337    }
5338}
5339
5340fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5341    let source_ref = source_ref_from_source_ref_range(path_source);
5342    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5343    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5344        return None;
5345    };
5346    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5347        return None;
5348    };
5349    if region_call.callee.name.name != "region" {
5350        return None;
5351    }
5352    Some(candidate)
5353}
5354
5355fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5356    let mut current_id = source_id;
5357    let mut current_composite = None;
5358    let mut visited = HashSet::new();
5359
5360    while visited.insert(current_id) {
5361        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5362
5363        let Some(composite_id) = next_composite_id else {
5364            break;
5365        };
5366        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5367            break;
5368        };
5369
5370        current_id = composite.id;
5371        current_composite = Some(composite);
5372
5373        if !composite.consumed {
5374            break;
5375        }
5376    }
5377
5378    current_composite.map(|composite| &composite.code_ref)
5379}
5380
5381fn downstream_composite_id_for_solid_source(
5382    artifact_graph: &ArtifactGraph,
5383    source_id: ArtifactId,
5384) -> Option<ArtifactId> {
5385    // Source is a path, find its solid.
5386    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5387        && let Some(composite_id) = path.composite_solid_id
5388        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5389        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5390    {
5391        return Some(composite_id);
5392    }
5393
5394    // Source is a sweep, find its path -> then find the solid
5395    for artifact in artifact_graph.values() {
5396        if let Artifact::Path(path) = artifact
5397            && path.sweep_id == Some(source_id)
5398            && let Some(composite_id) = path.composite_solid_id
5399            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5400            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5401        {
5402            return Some(composite_id);
5403        }
5404    }
5405
5406    // Source is a solid, find its downstream solid.
5407    artifact_graph.values().find_map(|artifact| {
5408        let Artifact::CompositeSolid(composite) = artifact else {
5409            return None;
5410        };
5411        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5412    })
5413}
5414
5415fn composite_contains_path_input(
5416    solid_ids: &[ArtifactId],
5417    tool_ids: &[ArtifactId],
5418    path_id: ArtifactId,
5419    solid2d_id: Option<ArtifactId>,
5420) -> bool {
5421    composite_contains_input(solid_ids, tool_ids, path_id)
5422        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5423}
5424
5425fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5426    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5427}
5428
5429fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5430    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5431    SourceRefRange {
5432        range: code_ref.range,
5433        node_path,
5434    }
5435}
5436
5437fn solid_output_index_for_sweep(
5438    artifact_graph: &ArtifactGraph,
5439    sweep_id: ArtifactId,
5440    sweep_code_ref: &CodeRef,
5441) -> Option<usize> {
5442    let sibling_sweeps = artifact_graph
5443        .values()
5444        .filter_map(|artifact| match artifact {
5445            Artifact::Sweep(sweep)
5446                if sweep.code_ref.range == sweep_code_ref.range
5447                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5448            {
5449                Some(sweep)
5450            }
5451            _ => None,
5452        })
5453        .collect::<Vec<_>>();
5454
5455    if sibling_sweeps.len() <= 1 {
5456        return None;
5457    }
5458
5459    sibling_sweeps
5460        .iter()
5461        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5462}
5463
5464fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5465    let mut existing_artifact_ids = scene_objects
5466        .iter()
5467        .map(|object| object.artifact_id)
5468        .collect::<HashSet<_>>();
5469
5470    for artifact in artifact_graph.values() {
5471        match artifact {
5472            Artifact::Wall(wall) => {
5473                if existing_artifact_ids.contains(&wall.id) {
5474                    continue;
5475                }
5476
5477                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5478                    Artifact::Segment(segment) => Some(segment),
5479                    _ => None,
5480                }) else {
5481                    continue;
5482                };
5483                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5484                    Artifact::Sweep(sweep) => Some(sweep),
5485                    _ => None,
5486                }) else {
5487                    continue;
5488                };
5489                let source_segment = segment
5490                    .original_seg_id
5491                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5492                    .and_then(|artifact| match artifact {
5493                        Artifact::Segment(segment) => Some(segment),
5494                        _ => None,
5495                    })
5496                    .unwrap_or(segment);
5497                let solid_code_ref =
5498                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5499                let path_code_ref = artifact_graph
5500                    .get(&segment.path_id)
5501                    .or_else(|| artifact_graph.get(&sweep.path_id))
5502                    .and_then(|artifact| match artifact {
5503                        Artifact::Path(path) => Some(&path.code_ref),
5504                        _ => None,
5505                    });
5506                let source = WallSource {
5507                    solid: code_ref_source_ref_range(solid_code_ref),
5508                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5509                    path: path_code_ref.map(code_ref_source_ref_range),
5510                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5511                };
5512                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5513                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5514                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5515                    .flatten();
5516                let object_source = source_ref_from_source_ref_range(&source.solid);
5517                let id = ObjectId(scene_objects.len());
5518                scene_objects.push(crate::front::Object {
5519                    id,
5520                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5521                        id,
5522                        source,
5523                        solid_output_index,
5524                    }),
5525                    label: Default::default(),
5526                    comments: Default::default(),
5527                    artifact_id: wall.id,
5528                    source: object_source,
5529                });
5530                existing_artifact_ids.insert(wall.id);
5531            }
5532            Artifact::Cap(cap) => {
5533                if existing_artifact_ids.contains(&cap.id) {
5534                    continue;
5535                }
5536
5537                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5538                    Artifact::Sweep(sweep) => Some(sweep),
5539                    _ => None,
5540                }) else {
5541                    continue;
5542                };
5543                let id = ObjectId(scene_objects.len());
5544                let kind = match cap.sub_type {
5545                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5546                    CapSubType::End => crate::frontend::api::CapKind::End,
5547                };
5548                let solid_code_ref =
5549                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5550                let source = CapSource {
5551                    solid: code_ref_source_ref_range(solid_code_ref),
5552                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5553                };
5554                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5555                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5556                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5557                    .flatten();
5558                let object_source = source_ref_from_source_ref_range(&source.solid);
5559                scene_objects.push(crate::front::Object {
5560                    id,
5561                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5562                        id,
5563                        kind,
5564                        source,
5565                        solid_output_index,
5566                    }),
5567                    label: Default::default(),
5568                    comments: Default::default(),
5569                    artifact_id: cap.id,
5570                    source: object_source,
5571                });
5572                existing_artifact_ids.insert(cap.id);
5573            }
5574            _ => {}
5575        }
5576    }
5577}
5578
5579fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5580    use crate::engine::PlaneName;
5581
5582    match name {
5583        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5584        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5585        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5586        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5587        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5588        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5589    }
5590}
5591
5592fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5593    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5594        ast::UnaryOperator::Neg,
5595        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5596    )))
5597}
5598
5599fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5600    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5601        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5602        unlabeled: Some(solid_expr),
5603        arguments: vec![ast::LabeledArg {
5604            label: Some(ast::Identifier::new("face")),
5605            arg: face_expr,
5606        }],
5607        digest: None,
5608        non_code_meta: Default::default(),
5609    })))
5610}
5611
5612fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5613    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5614        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5615        unlabeled: Some(solid_expr),
5616        arguments: vec![ast::LabeledArg {
5617            label: Some(ast::Identifier::new("index")),
5618            arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
5619                value: index as f64,
5620                suffix: NumericSuffix::None,
5621                raw: index.to_string(),
5622                digest: None,
5623            })))),
5624        }],
5625        digest: None,
5626        non_code_meta: Default::default(),
5627    })))
5628}
5629
5630fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5631    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5632        return None;
5633    };
5634    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5635        return None;
5636    };
5637    if !matches!(
5638        sweep_call.callee.name.name.as_str(),
5639        "extrude" | "revolve" | "sweep" | "loft"
5640    ) {
5641        return None;
5642    }
5643    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5644        return None;
5645    };
5646    let candidate = region_name_expr.name.name.clone();
5647    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5648        return None;
5649    };
5650    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5651        return None;
5652    };
5653    if region_call.callee.name.name != "region" {
5654        return None;
5655    }
5656    Some(candidate)
5657}
5658
5659/// Return the AST expression referencing the variable at the given source ref.
5660/// If no such variable exists, insert a new variable declaration with the given
5661/// prefix.
5662///
5663/// This may return a complex expression referencing properties of the variable
5664/// (e.g., `line1.start`).
5665fn get_or_insert_ast_reference(
5666    ast: &mut ast::Node<ast::Program>,
5667    source_ref: &SourceRef,
5668    prefix: &str,
5669    property: Option<&str>,
5670) -> Result<ast::Expr, KclError> {
5671    let command = AstMutateCommand::AddVariableDeclaration {
5672        prefix: prefix.to_owned(),
5673    };
5674    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5675        Ok((_, ret)) => ret,
5676        Err(err) => {
5677            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5678                AstMutateCommandReturn::Name(var_name)
5679            } else {
5680                return Err(err);
5681            }
5682        }
5683    };
5684    let AstMutateCommandReturn::Name(var_name) = ret else {
5685        return Err(KclError::refactor(
5686            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5687        ));
5688    };
5689    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5690    let Some(property) = property else {
5691        // No property; just return the variable name.
5692        return Ok(var_expr);
5693    };
5694
5695    Ok(create_member_expression(var_expr, property))
5696}
5697
5698fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5699    let source_range = match source_ref {
5700        SourceRef::Simple { range, .. } => *range,
5701        SourceRef::BackTrace { ranges } => {
5702            let [range] = ranges.as_slice() else {
5703                return None;
5704            };
5705            range.0
5706        }
5707    };
5708    ast.body.iter().find_map(|item| {
5709        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5710            return None;
5711        };
5712        let init_range = SourceRange::from(&var_decl.declaration.init);
5713        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5714            && init_range.start() <= source_range.start()
5715            && source_range.end() <= init_range.end();
5716        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5717            && init_range != source_range
5718            && source_is_inside_init
5719        {
5720            return None;
5721        }
5722        source_is_inside_init.then(|| var_decl.name().to_owned())
5723    })
5724}
5725
5726fn mutate_ast_node_by_source_ref(
5727    ast: &mut ast::Node<ast::Program>,
5728    source_ref: &SourceRef,
5729    command: AstMutateCommand,
5730) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5731    let (source_range, node_path) = match source_ref {
5732        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5733        SourceRef::BackTrace { ranges } => {
5734            let [range] = ranges.as_slice() else {
5735                return Err(KclError::refactor(format!(
5736                    "Expected single source ref, got {}; ranges={ranges:#?}",
5737                    ranges.len(),
5738                )));
5739            };
5740            (range.0, range.1.clone())
5741        }
5742    };
5743    let mut context = AstMutateContext {
5744        source_range,
5745        node_path,
5746        command,
5747        defined_names_stack: Default::default(),
5748    };
5749    let control = dfs_mut(ast, &mut context);
5750    match control {
5751        ControlFlow::Continue(_) => Err(KclError::refactor(
5752            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5753        )),
5754        ControlFlow::Break(break_value) => break_value,
5755    }
5756}
5757
5758#[derive(Debug)]
5759struct AstMutateContext {
5760    source_range: SourceRange,
5761    node_path: Option<ast::NodePath>,
5762    command: AstMutateCommand,
5763    defined_names_stack: Vec<HashSet<String>>,
5764}
5765
5766#[derive(Debug)]
5767#[allow(clippy::large_enum_variant)]
5768enum AstMutateCommand {
5769    /// Add an expression statement to the sketch block.
5770    AddSketchBlockExprStmt {
5771        expr: ast::Expr,
5772    },
5773    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5774    AddSketchBlockVarDecl {
5775        prefix: String,
5776        expr: ast::Expr,
5777    },
5778    AddVariableDeclaration {
5779        prefix: String,
5780    },
5781    EditPoint {
5782        at: ast::Expr,
5783    },
5784    EditLine {
5785        start: ast::Expr,
5786        end: ast::Expr,
5787        construction: Option<bool>,
5788    },
5789    EditArc {
5790        start: ast::Expr,
5791        end: ast::Expr,
5792        center: ast::Expr,
5793        construction: Option<bool>,
5794    },
5795    EditCircle {
5796        start: ast::Expr,
5797        center: ast::Expr,
5798        construction: Option<bool>,
5799    },
5800    EditControlPointSpline {
5801        points: ast::Expr,
5802        construction: Option<bool>,
5803    },
5804    EditConstraintValue {
5805        value: ast::BinaryPart,
5806    },
5807    EditDistanceConstraintLabelPosition {
5808        label_position: ast::Expr,
5809    },
5810    EditCallUnlabeled {
5811        arg: ast::Expr,
5812    },
5813    EditVarInitialValue {
5814        value: Number,
5815    },
5816    DeleteNode,
5817}
5818
5819impl AstMutateCommand {
5820    fn needs_defined_names_stack(&self) -> bool {
5821        matches!(
5822            self,
5823            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5824        )
5825    }
5826}
5827
5828#[derive(Debug)]
5829enum AstMutateCommandReturn {
5830    None,
5831    Name(String),
5832}
5833
5834#[derive(Debug, Clone)]
5835struct AstNodeRef {
5836    range: SourceRange,
5837    node_path: Option<ast::NodePath>,
5838}
5839
5840impl<T> From<&ast::Node<T>> for AstNodeRef {
5841    fn from(value: &ast::Node<T>) -> Self {
5842        AstNodeRef {
5843            range: value.into(),
5844            node_path: value.node_path.clone(),
5845        }
5846    }
5847}
5848
5849impl From<&ast::BodyItem> for AstNodeRef {
5850    fn from(value: &ast::BodyItem) -> Self {
5851        match value {
5852            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5853                range: node.into(),
5854                node_path: node.node_path.clone(),
5855            },
5856            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5857                range: node.into(),
5858                node_path: node.node_path.clone(),
5859            },
5860            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5861                range: node.into(),
5862                node_path: node.node_path.clone(),
5863            },
5864            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5865                range: node.into(),
5866                node_path: node.node_path.clone(),
5867            },
5868            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5869                range: node.into(),
5870                node_path: node.node_path.clone(),
5871            },
5872        }
5873    }
5874}
5875
5876impl From<&ast::Expr> for AstNodeRef {
5877    fn from(value: &ast::Expr) -> Self {
5878        AstNodeRef {
5879            range: SourceRange::from(value),
5880            node_path: value.node_path().cloned(),
5881        }
5882    }
5883}
5884
5885impl From<&AstMutateContext> for AstNodeRef {
5886    fn from(value: &AstMutateContext) -> Self {
5887        AstNodeRef {
5888            range: value.source_range,
5889            node_path: value.node_path.clone(),
5890        }
5891    }
5892}
5893
5894impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5895    type Error = crate::walk::AstNodeError;
5896
5897    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5898        Ok(AstNodeRef {
5899            range: SourceRange::try_from(value)?,
5900            node_path: value.try_into()?,
5901        })
5902    }
5903}
5904
5905impl From<AstNodeRef> for SourceRange {
5906    fn from(value: AstNodeRef) -> Self {
5907        value.range
5908    }
5909}
5910
5911impl Visitor for AstMutateContext {
5912    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5913    type Continue = ();
5914
5915    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5916        filter_and_process(self, node)
5917    }
5918
5919    fn finish(&mut self, node: NodeMut<'_>) {
5920        match &node {
5921            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5922                self.defined_names_stack.pop();
5923            }
5924            _ => {}
5925        }
5926    }
5927}
5928
5929fn filter_and_process(
5930    ctx: &mut AstMutateContext,
5931    node: NodeMut,
5932) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5933    let Ok(node_range) = SourceRange::try_from(&node) else {
5934        // Nodes that can't be converted to a range aren't interesting.
5935        return TraversalReturn::new_continue(());
5936    };
5937    // If we're adding a variable declaration, we need to look at variable
5938    // declaration expressions to see if it already has a variable, before
5939    // continuing. The variable declaration's source range won't match the
5940    // target; its init expression will.
5941    if let NodeMut::VariableDeclaration(var_decl) = &node {
5942        let expr_range = SourceRange::from(&var_decl.declaration.init);
5943        let expr_node_path = var_decl.declaration.init.node_path();
5944        if source_ref_matches(ctx, expr_range, expr_node_path) {
5945            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5946                // We found the variable declaration expression. It doesn't need
5947                // to be added.
5948                return TraversalReturn::new_break(Ok((
5949                    AstNodeRef::from(&**var_decl),
5950                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5951                )));
5952            }
5953            if let AstMutateCommand::DeleteNode = &ctx.command {
5954                // We found the variable declaration. Delete the variable along
5955                // with the segment.
5956                return TraversalReturn {
5957                    mutate_body_item: MutateBodyItem::Delete,
5958                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5959                };
5960            }
5961        }
5962    }
5963    // Similar thing with expression statement. We need to look at the
5964    // expression inside it.
5965    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5966        let expr_range = SourceRange::from(&expr_stmt.expression);
5967        let expr_node_path = expr_stmt.expression.node_path();
5968        if source_ref_matches(ctx, expr_range, expr_node_path) {
5969            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5970                // We found the node wrapped in an expression statement. Process
5971                // the statement.
5972                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5973                    return TraversalReturn::new_continue(());
5974                };
5975                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5976            }
5977            if let AstMutateCommand::DeleteNode = &ctx.command {
5978                // We found the node wrapped in an expression statement. Delete
5979                // the whole statement.
5980                return TraversalReturn {
5981                    mutate_body_item: MutateBodyItem::Delete,
5982                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5983                };
5984            }
5985        }
5986    }
5987
5988    if ctx.command.needs_defined_names_stack() {
5989        if let NodeMut::Program(program) = &node {
5990            ctx.defined_names_stack.push(find_defined_names(*program));
5991        } else if let NodeMut::SketchBlock(block) = &node {
5992            ctx.defined_names_stack.push(find_defined_names(&block.body));
5993        }
5994    }
5995
5996    // Make sure the node matches the source ref.
5997    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5998    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5999        return TraversalReturn::new_continue(());
6000    }
6001    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6002        return TraversalReturn::new_continue(());
6003    };
6004    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6005}
6006
6007fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6008    match &ctx.node_path {
6009        Some(target) => Some(target) == node_path,
6010        None => node_range == ctx.source_range,
6011    }
6012}
6013
6014fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6015    match &ctx.command {
6016        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6017            if let NodeMut::SketchBlock(sketch_block) = node {
6018                sketch_block
6019                    .body
6020                    .items
6021                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6022                        inner: ast::ExpressionStatement {
6023                            expression: expr.clone(),
6024                            digest: None,
6025                        },
6026                        start: Default::default(),
6027                        end: Default::default(),
6028                        module_id: Default::default(),
6029                        node_path: None,
6030                        outer_attrs: Default::default(),
6031                        pre_comments: Default::default(),
6032                        comment_start: Default::default(),
6033                    }));
6034                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6035            }
6036        }
6037        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6038            if let NodeMut::SketchBlock(sketch_block) = node {
6039                let empty_defined_names = HashSet::new();
6040                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6041                let Ok(name) = next_free_name(prefix, defined_names) else {
6042                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6043                };
6044                sketch_block
6045                    .body
6046                    .items
6047                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
6048                        ast::VariableDeclaration::new(
6049                            ast::VariableDeclarator::new(&name, expr.clone()),
6050                            ast::ItemVisibility::Default,
6051                            ast::VariableKind::Const,
6052                        ),
6053                    ))));
6054                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6055            }
6056        }
6057        AstMutateCommand::AddVariableDeclaration { prefix } => {
6058            if let NodeMut::VariableDeclaration(inner) = node {
6059                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6060            }
6061            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6062                let empty_defined_names = HashSet::new();
6063                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6064                let Ok(name) = next_free_name(prefix, defined_names) else {
6065                    // TODO: Return an error instead?
6066                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6067                };
6068                let mutate_node =
6069                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
6070                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6071                        ast::ItemVisibility::Default,
6072                        ast::VariableKind::Const,
6073                    ))));
6074                return TraversalReturn {
6075                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6076                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6077                };
6078            }
6079        }
6080        AstMutateCommand::EditPoint { at } => {
6081            if let NodeMut::CallExpressionKw(call) = node {
6082                if call.callee.name.name != POINT_FN {
6083                    return TraversalReturn::new_continue(());
6084                }
6085                // Update the arguments.
6086                for labeled_arg in &mut call.arguments {
6087                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6088                        labeled_arg.arg = at.clone();
6089                    }
6090                }
6091                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6092            }
6093        }
6094        AstMutateCommand::EditLine {
6095            start,
6096            end,
6097            construction,
6098        } => {
6099            if let NodeMut::CallExpressionKw(call) = node {
6100                if call.callee.name.name != LINE_FN {
6101                    return TraversalReturn::new_continue(());
6102                }
6103                // Update the arguments.
6104                for labeled_arg in &mut call.arguments {
6105                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6106                        labeled_arg.arg = start.clone();
6107                    }
6108                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6109                        labeled_arg.arg = end.clone();
6110                    }
6111                }
6112                // Handle construction kwarg
6113                if let Some(construction_value) = construction {
6114                    let construction_exists = call
6115                        .arguments
6116                        .iter()
6117                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6118                    if *construction_value {
6119                        // Add or update construction=true
6120                        if construction_exists {
6121                            // Update existing construction kwarg
6122                            for labeled_arg in &mut call.arguments {
6123                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6124                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6125                                        value: ast::LiteralValue::Bool(true),
6126                                        raw: "true".to_string(),
6127                                        digest: None,
6128                                    })));
6129                                }
6130                            }
6131                        } else {
6132                            // Add new construction kwarg
6133                            call.arguments.push(ast::LabeledArg {
6134                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6135                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6136                                    value: ast::LiteralValue::Bool(true),
6137                                    raw: "true".to_string(),
6138                                    digest: None,
6139                                }))),
6140                            });
6141                        }
6142                    } else {
6143                        // Remove construction kwarg if it exists
6144                        call.arguments
6145                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6146                    }
6147                }
6148                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6149            }
6150        }
6151        AstMutateCommand::EditArc {
6152            start,
6153            end,
6154            center,
6155            construction,
6156        } => {
6157            if let NodeMut::CallExpressionKw(call) = node {
6158                if call.callee.name.name != ARC_FN {
6159                    return TraversalReturn::new_continue(());
6160                }
6161                // Update the arguments.
6162                for labeled_arg in &mut call.arguments {
6163                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6164                        labeled_arg.arg = start.clone();
6165                    }
6166                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6167                        labeled_arg.arg = end.clone();
6168                    }
6169                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6170                        labeled_arg.arg = center.clone();
6171                    }
6172                }
6173                // Handle construction kwarg
6174                if let Some(construction_value) = construction {
6175                    let construction_exists = call
6176                        .arguments
6177                        .iter()
6178                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6179                    if *construction_value {
6180                        // Add or update construction=true
6181                        if construction_exists {
6182                            // Update existing construction kwarg
6183                            for labeled_arg in &mut call.arguments {
6184                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6185                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6186                                        value: ast::LiteralValue::Bool(true),
6187                                        raw: "true".to_string(),
6188                                        digest: None,
6189                                    })));
6190                                }
6191                            }
6192                        } else {
6193                            // Add new construction kwarg
6194                            call.arguments.push(ast::LabeledArg {
6195                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6196                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6197                                    value: ast::LiteralValue::Bool(true),
6198                                    raw: "true".to_string(),
6199                                    digest: None,
6200                                }))),
6201                            });
6202                        }
6203                    } else {
6204                        // Remove construction kwarg if it exists
6205                        call.arguments
6206                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6207                    }
6208                }
6209                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6210            }
6211        }
6212        AstMutateCommand::EditCircle {
6213            start,
6214            center,
6215            construction,
6216        } => {
6217            if let NodeMut::CallExpressionKw(call) = node {
6218                if call.callee.name.name != CIRCLE_FN {
6219                    return TraversalReturn::new_continue(());
6220                }
6221                // Update the arguments.
6222                for labeled_arg in &mut call.arguments {
6223                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6224                        labeled_arg.arg = start.clone();
6225                    }
6226                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6227                        labeled_arg.arg = center.clone();
6228                    }
6229                }
6230                // Handle construction kwarg
6231                if let Some(construction_value) = construction {
6232                    let construction_exists = call
6233                        .arguments
6234                        .iter()
6235                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6236                    if *construction_value {
6237                        if construction_exists {
6238                            // Update existing construction kwarg
6239                            for labeled_arg in &mut call.arguments {
6240                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6241                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6242                                        value: ast::LiteralValue::Bool(true),
6243                                        raw: "true".to_string(),
6244                                        digest: None,
6245                                    })));
6246                                }
6247                            }
6248                        } else {
6249                            // Add new construction kwarg
6250                            call.arguments.push(ast::LabeledArg {
6251                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6252                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6253                                    value: ast::LiteralValue::Bool(true),
6254                                    raw: "true".to_string(),
6255                                    digest: None,
6256                                }))),
6257                            });
6258                        }
6259                    } else {
6260                        // Remove construction kwarg if it exists
6261                        call.arguments
6262                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6263                    }
6264                }
6265                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6266            }
6267        }
6268        AstMutateCommand::EditControlPointSpline { points, construction } => {
6269            if let NodeMut::CallExpressionKw(call) = node {
6270                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6271                    return TraversalReturn::new_continue(());
6272                }
6273                for labeled_arg in &mut call.arguments {
6274                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6275                    {
6276                        labeled_arg.arg = points.clone();
6277                    }
6278                }
6279                // Handle construction kwarg
6280                if let Some(construction_value) = construction {
6281                    let construction_exists = call
6282                        .arguments
6283                        .iter()
6284                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6285                    if *construction_value {
6286                        if construction_exists {
6287                            for labeled_arg in &mut call.arguments {
6288                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6289                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6290                                        value: ast::LiteralValue::Bool(true),
6291                                        raw: "true".to_string(),
6292                                        digest: None,
6293                                    })));
6294                                }
6295                            }
6296                        } else {
6297                            call.arguments.push(ast::LabeledArg {
6298                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6299                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6300                                    value: ast::LiteralValue::Bool(true),
6301                                    raw: "true".to_string(),
6302                                    digest: None,
6303                                }))),
6304                            });
6305                        }
6306                    } else {
6307                        call.arguments
6308                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6309                    }
6310                }
6311                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6312            }
6313        }
6314        AstMutateCommand::EditConstraintValue { value } => {
6315            if let NodeMut::BinaryExpression(binary_expr) = node {
6316                let left_is_constraint = matches!(
6317                    &binary_expr.left,
6318                    ast::BinaryPart::CallExpressionKw(call)
6319                        if matches!(
6320                            call.callee.name.name.as_str(),
6321                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6322                        )
6323                );
6324                if left_is_constraint {
6325                    binary_expr.right = value.clone();
6326                } else {
6327                    binary_expr.left = value.clone();
6328                }
6329
6330                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6331            }
6332        }
6333        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6334            if let NodeMut::BinaryExpression(binary_expr) = node {
6335                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6336                    return TraversalReturn::new_continue(());
6337                };
6338                if !matches!(
6339                    call.callee.name.name.as_str(),
6340                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6341                ) {
6342                    return TraversalReturn::new_continue(());
6343                }
6344
6345                if let Some(label_arg) = call
6346                    .arguments
6347                    .iter_mut()
6348                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6349                {
6350                    label_arg.arg = label_position.clone();
6351                } else {
6352                    call.arguments.push(ast::LabeledArg {
6353                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6354                        arg: label_position.clone(),
6355                    });
6356                }
6357
6358                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6359            }
6360        }
6361        AstMutateCommand::EditCallUnlabeled { arg } => {
6362            if let NodeMut::CallExpressionKw(call) = node {
6363                call.unlabeled = Some(arg.clone());
6364                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6365            }
6366        }
6367        AstMutateCommand::EditVarInitialValue { value } => {
6368            // We target the SketchVar itself (matched by NodePath) rather than
6369            // the inner NumericLiteral so we can also write back into vars that
6370            // were declared without an initial value (e.g. bare `var`).
6371            if let NodeMut::SketchVar(sketch_var) = node {
6372                let Ok(literal) = to_source_number(*value) else {
6373                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6374                        "Could not convert number to AST literal: {:?}",
6375                        *value
6376                    ))));
6377                };
6378                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6379                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6380            }
6381        }
6382        AstMutateCommand::DeleteNode => {
6383            return TraversalReturn {
6384                mutate_body_item: MutateBodyItem::Delete,
6385                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6386            };
6387        }
6388    }
6389    TraversalReturn::new_continue(())
6390}
6391
6392struct FindSketchBlockSourceRange {
6393    /// The source range of the sketch block before mutation.
6394    target_before_mutation: SourceRange,
6395    /// The source range of the sketch block's last body item after mutation. We
6396    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6397    /// shared reference.
6398    found: Cell<Option<AstNodeRef>>,
6399}
6400
6401impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6402    type Error = crate::front::Error;
6403
6404    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6405        let Ok(node_range) = SourceRange::try_from(&node) else {
6406            return Ok(true);
6407        };
6408
6409        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6410            if node_range.module_id() == self.target_before_mutation.module_id()
6411                && node_range.start() == self.target_before_mutation.start()
6412                // End shouldn't match since we added something.
6413                && node_range.end() >= self.target_before_mutation.end()
6414            {
6415                self.found.set(sketch_block.body.items.last().map(|item| match item {
6416                    // For declarations like `circle1 = circle(...)`, use
6417                    // the init expression range so lookup in source_range_to_object
6418                    // matches the segment source range.
6419                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6420                    _ => AstNodeRef::from(item),
6421                }));
6422                return Ok(false);
6423            } else {
6424                // We found a different sketch block. No need to descend into
6425                // its children since sketch blocks cannot be nested.
6426                return Ok(true);
6427            }
6428        }
6429
6430        for child in node.children().iter() {
6431            if !child.visit(*self)? {
6432                return Ok(false);
6433            }
6434        }
6435
6436        Ok(true)
6437    }
6438}
6439
6440struct FindSketchBlockByNodePath {
6441    /// The Node Path of the sketch block before mutation.
6442    target_node_path: ast::NodePath,
6443    /// The ref of the sketch block's last body item after mutation. We need to
6444    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6445    /// reference.
6446    found: Cell<Option<AstNodeRef>>,
6447}
6448
6449impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6450    type Error = crate::front::Error;
6451
6452    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6453        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6454            return Ok(true);
6455        };
6456
6457        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6458            if let Some(node_path) = node_path
6459                && node_path == self.target_node_path
6460            {
6461                self.found.set(sketch_block.body.items.last().map(|item| match item {
6462                    // For declarations like `circle1 = circle(...)`, use
6463                    // the init expression range so lookup in source_range_to_object
6464                    // matches the segment source range.
6465                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6466                    _ => AstNodeRef::from(item),
6467                }));
6468
6469                return Ok(false);
6470            } else {
6471                // We found a different sketch block. No need to descend into
6472                // its children since sketch blocks cannot be nested.
6473                return Ok(true);
6474            }
6475        }
6476
6477        for child in node.children().iter() {
6478            if !child.visit(*self)? {
6479                return Ok(false);
6480            }
6481        }
6482
6483        Ok(true)
6484    }
6485}
6486
6487/// After adding an item to a sketch block, find the sketch block, and get the
6488/// source range of the added item. We assume that the added item is the last
6489/// item in the sketch block and that the sketch block's source range has grown,
6490/// but not moved from its starting offset.
6491///
6492/// TODO: Do we need to format *before* mutation in case formatting moves the
6493/// sketch block forward?
6494fn find_sketch_block_added_item(
6495    ast: &ast::Node<ast::Program>,
6496    sketch_block_before_mutation: &AstNodeRef,
6497) -> Result<AstNodeRef, KclError> {
6498    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6499        let find = FindSketchBlockByNodePath {
6500            target_node_path: node_path.clone(),
6501            found: Cell::new(None),
6502        };
6503        let node = crate::walk::Node::from(ast);
6504        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6505        find.found.into_inner().ok_or_else(|| {
6506            KclError::refactor(format!(
6507                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6508            ))
6509        })
6510    } else {
6511        // No NodePath. Fall back to legacy source range.
6512        let find = FindSketchBlockSourceRange {
6513            target_before_mutation: sketch_block_before_mutation.range,
6514            found: Cell::new(None),
6515        };
6516        let node = crate::walk::Node::from(ast);
6517        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6518        find.found.into_inner().ok_or_else(|| KclError::refactor(
6519            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?"),
6520        ))
6521    }
6522}
6523
6524fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6525    let message = error.message().trim();
6526    let message = if message.is_empty() {
6527        "unknown parse error"
6528    } else {
6529        message
6530    };
6531
6532    format!("{prefix}: {message}")
6533}
6534
6535fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6536    let (program, errors) = Program::parse(source).map_err(|err| {
6537        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6538    })?;
6539    if !errors.is_empty() {
6540        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6541            format_compilation_issues(parse_error_prefix, &errors),
6542        )));
6543    }
6544
6545    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6546}
6547
6548fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6549    let Some(first_issue) = issues
6550        .iter()
6551        .find(|issue| issue.severity.is_err())
6552        .or_else(|| issues.first())
6553    else {
6554        return prefix.to_owned();
6555    };
6556
6557    let message = first_issue.message.trim();
6558    let message = if message.is_empty() {
6559        "unknown parse error"
6560    } else {
6561        message
6562    };
6563
6564    if issues.len() > 1 {
6565        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6566    } else {
6567        format!("{prefix}: {message}")
6568    }
6569}
6570
6571fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6572    // TODO: Don't duplicate this from lib.rs Program.
6573    ast.recast_top(&Default::default(), 0)
6574}
6575
6576struct FindNumericLiteral {
6577    target: SourceRange,
6578    found: Cell<Option<ast::NumericLiteral>>,
6579}
6580
6581impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6582    type Error = crate::front::Error;
6583
6584    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6585        let Ok(node_range) = SourceRange::try_from(&node) else {
6586            return Ok(true);
6587        };
6588
6589        if node_range == self.target
6590            && let crate::walk::Node::NumericLiteral(literal) = node
6591        {
6592            self.found.set(Some(literal.inner.clone()));
6593            return Ok(false);
6594        }
6595
6596        for child in node.children().iter() {
6597            if !child.visit(*self)? {
6598                return Ok(false);
6599            }
6600        }
6601
6602        Ok(true)
6603    }
6604}
6605
6606fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6607    let find = FindNumericLiteral {
6608        target,
6609        found: Cell::new(None),
6610    };
6611    let node = crate::walk::Node::from(ast);
6612    node.visit(&find).ok()?;
6613    find.found.into_inner()
6614}
6615
6616struct FindSketchVarInitialByNodePath<'a> {
6617    target: &'a ast::NodePath,
6618    sketch_var_found: Cell<bool>,
6619    initial_literal: Cell<Option<ast::NumericLiteral>>,
6620}
6621
6622impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6623    type Error = crate::front::Error;
6624
6625    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6626        if let crate::walk::Node::SketchVar(sketch_var) = node
6627            && sketch_var.node_path.as_ref() == Some(self.target)
6628        {
6629            self.sketch_var_found.set(true);
6630            if let Some(initial) = &sketch_var.initial {
6631                self.initial_literal.set(Some(initial.inner.clone()));
6632            }
6633            return Ok(false);
6634        }
6635
6636        for child in node.children().iter() {
6637            if !child.visit(*self)? {
6638                return Ok(false);
6639            }
6640        }
6641
6642        Ok(true)
6643    }
6644}
6645
6646/// Locate the source `var` declaration corresponding to a sketch-var solution.
6647///
6648/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6649/// from "target found." The inner [`Option`] is the initial numeric literal of
6650/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6651///
6652/// When `node_path` is `None` (e.g. for older outcomes that predate the
6653/// node-path propagation), this falls back to source-range matching, which
6654/// can break under whitespace shifts elsewhere in the file.
6655fn numeric_literal_at_node_path(
6656    ast: &ast::Node<ast::Program>,
6657    node_path: Option<&ast::NodePath>,
6658    source_range: SourceRange,
6659) -> Option<Option<ast::NumericLiteral>> {
6660    let Some(node_path) = node_path else {
6661        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";
6662        #[cfg(target_arch = "wasm32")]
6663        web_sys::console::warn_1(&message.into());
6664        #[cfg(not(target_arch = "wasm32"))]
6665        eprintln!("WARNING: {message}");
6666        return numeric_literal_at_source_range(ast, source_range).map(Some);
6667    };
6668    let find = FindSketchVarInitialByNodePath {
6669        target: node_path,
6670        sketch_var_found: Cell::new(false),
6671        initial_literal: Cell::new(None),
6672    };
6673    let node = crate::walk::Node::from(ast);
6674    node.visit(&find).ok()?;
6675    if !find.sketch_var_found.get() {
6676        return None;
6677    }
6678    Some(find.initial_literal.into_inner())
6679}
6680
6681fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6682    match suffix {
6683        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6684        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6685        NumericSuffix::M => Some(UnitLength::Meters),
6686        NumericSuffix::Inch => Some(UnitLength::Inches),
6687        NumericSuffix::Ft => Some(UnitLength::Feet),
6688        NumericSuffix::Yd => Some(UnitLength::Yards),
6689        _ => None,
6690    }
6691}
6692
6693fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6694    match suffix_length_unit(number.units) {
6695        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6696        None => number.value,
6697    }
6698}
6699
6700fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6701    match suffix_length_unit(literal.suffix) {
6702        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6703        None => literal.value,
6704    }
6705}
6706
6707fn var_solution_needs_commit(
6708    current_literal: &ast::NumericLiteral,
6709    solved_value: Number,
6710    default_length_unit: UnitLength,
6711) -> bool {
6712    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6713    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6714
6715    (current - solved).abs() > 1e-9
6716}
6717
6718fn preserve_var_solution_literal_style(
6719    current_literal: &ast::NumericLiteral,
6720    solved_value: Number,
6721    default_length_unit: UnitLength,
6722) -> Number {
6723    if current_literal.suffix == NumericSuffix::None {
6724        return Number {
6725            value: number_value_in_default_length_units(solved_value, default_length_unit),
6726            units: NumericSuffix::None,
6727        };
6728    }
6729
6730    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6731        return solved_value;
6732    };
6733
6734    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6735    Number {
6736        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6737        units: current_literal.suffix,
6738    }
6739}
6740
6741pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6742    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6743        inner: ast::ArrayExpression {
6744            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6745            non_code_meta: Default::default(),
6746            digest: None,
6747        },
6748        start: Default::default(),
6749        end: Default::default(),
6750        module_id: Default::default(),
6751        node_path: None,
6752        outer_attrs: Default::default(),
6753        pre_comments: Default::default(),
6754        comment_start: Default::default(),
6755    })))
6756}
6757
6758pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6759    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6760        ast::ArrayExpression {
6761            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6762            digest: None,
6763            non_code_meta: Default::default(),
6764        },
6765    ))))
6766}
6767
6768fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6769    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6770        ast::ArrayExpression {
6771            elements: vec![
6772                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6773                    point.x,
6774                )?)))),
6775                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6776                    point.y,
6777                )?)))),
6778            ],
6779            non_code_meta: Default::default(),
6780            digest: None,
6781        },
6782    ))))
6783}
6784
6785fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6786    match expr {
6787        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6788            inner: ast::Literal::from(to_source_number(*number)?),
6789            start: Default::default(),
6790            end: Default::default(),
6791            module_id: Default::default(),
6792            node_path: None,
6793            outer_attrs: Default::default(),
6794            pre_comments: Default::default(),
6795            comment_start: Default::default(),
6796        }))),
6797        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6798            inner: ast::SketchVar {
6799                initial: Some(Box::new(ast::Node {
6800                    inner: to_source_number(*number)?,
6801                    start: Default::default(),
6802                    end: Default::default(),
6803                    module_id: Default::default(),
6804                    node_path: None,
6805                    outer_attrs: Default::default(),
6806                    pre_comments: Default::default(),
6807                    comment_start: Default::default(),
6808                })),
6809                digest: None,
6810            },
6811            start: Default::default(),
6812            end: Default::default(),
6813            module_id: Default::default(),
6814            node_path: None,
6815            outer_attrs: Default::default(),
6816            pre_comments: Default::default(),
6817            comment_start: Default::default(),
6818        }))),
6819        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6820    }
6821}
6822
6823fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6824    Ok(ast::NumericLiteral {
6825        value: number.value,
6826        suffix: number.units,
6827        raw: format_number_literal(number.value, number.units, None)?,
6828        digest: None,
6829    })
6830}
6831
6832pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6833    ast::Expr::Name(Box::new(ast_name(name)))
6834}
6835
6836fn ast_name(name: String) -> ast::Node<ast::Name> {
6837    ast::Node {
6838        inner: ast::Name {
6839            name: ast::Node {
6840                inner: ast::Identifier { name, digest: None },
6841                start: Default::default(),
6842                end: Default::default(),
6843                module_id: Default::default(),
6844                node_path: None,
6845                outer_attrs: Default::default(),
6846                pre_comments: Default::default(),
6847                comment_start: Default::default(),
6848            },
6849            path: Vec::new(),
6850            abs_path: false,
6851            digest: None,
6852        },
6853        start: Default::default(),
6854        end: Default::default(),
6855        module_id: Default::default(),
6856        node_path: None,
6857        outer_attrs: Default::default(),
6858        pre_comments: Default::default(),
6859        comment_start: Default::default(),
6860    }
6861}
6862
6863pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6864    ast::Name {
6865        name: ast::Node {
6866            inner: ast::Identifier {
6867                name: name.to_owned(),
6868                digest: None,
6869            },
6870            start: Default::default(),
6871            end: Default::default(),
6872            module_id: Default::default(),
6873            node_path: None,
6874            outer_attrs: Default::default(),
6875            pre_comments: Default::default(),
6876            comment_start: Default::default(),
6877        },
6878        path: Default::default(),
6879        abs_path: false,
6880        digest: None,
6881    }
6882}
6883
6884// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6885
6886/// Create an AST node for coincident([expr1, expr2, ...])
6887pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6888    let elements = exprs.into_iter().collect::<Vec<_>>();
6889    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6890
6891    // Create array [expr1, expr2, ...]
6892    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6893        elements,
6894        digest: None,
6895        non_code_meta: Default::default(),
6896    })));
6897
6898    // Create coincident([...])
6899    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6900        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6901        unlabeled: Some(array_expr),
6902        arguments: Default::default(),
6903        digest: None,
6904        non_code_meta: Default::default(),
6905    })))
6906}
6907
6908/// Create an AST node for line(start = [...], end = [...])
6909pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6910    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6911        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6912        unlabeled: None,
6913        arguments: vec![
6914            ast::LabeledArg {
6915                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6916                arg: start_ast,
6917            },
6918            ast::LabeledArg {
6919                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6920                arg: end_ast,
6921            },
6922        ],
6923        digest: None,
6924        non_code_meta: Default::default(),
6925    })))
6926}
6927
6928/// Create an AST node for arc(start = [...], end = [...], center = [...])
6929pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6930    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6931        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6932        unlabeled: None,
6933        arguments: vec![
6934            ast::LabeledArg {
6935                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6936                arg: start_ast,
6937            },
6938            ast::LabeledArg {
6939                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6940                arg: end_ast,
6941            },
6942            ast::LabeledArg {
6943                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6944                arg: center_ast,
6945            },
6946        ],
6947        digest: None,
6948        non_code_meta: Default::default(),
6949    })))
6950}
6951
6952/// Create an AST node for circle(start = [...], center = [...])
6953pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6954    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6955        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6956        unlabeled: None,
6957        arguments: vec![
6958            ast::LabeledArg {
6959                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6960                arg: start_ast,
6961            },
6962            ast::LabeledArg {
6963                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6964                arg: center_ast,
6965            },
6966        ],
6967        digest: None,
6968        non_code_meta: Default::default(),
6969    })))
6970}
6971
6972/// Create an AST node for horizontal(line)
6973pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6974    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6975        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6976        unlabeled: Some(line_expr),
6977        arguments: Default::default(),
6978        digest: None,
6979        non_code_meta: Default::default(),
6980    })))
6981}
6982
6983/// Create an AST node for vertical(line)
6984pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6985    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6986        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6987        unlabeled: Some(line_expr),
6988        arguments: Default::default(),
6989        digest: None,
6990        non_code_meta: Default::default(),
6991    })))
6992}
6993
6994/// Create a member expression like object.property (e.g., line1.end)
6995pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6996    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6997        object: object_expr,
6998        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6999            name: ast::Node::no_src(ast::Identifier {
7000                name: property.to_string(),
7001                digest: None,
7002            }),
7003            path: Vec::new(),
7004            abs_path: false,
7005            digest: None,
7006        }))),
7007        computed: false,
7008        digest: None,
7009    })))
7010}
7011
7012pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7013    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
7014        object: object_expr,
7015        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
7016            value: index as f64,
7017            suffix: NumericSuffix::None,
7018            raw: index.to_string(),
7019            digest: None,
7020        })))),
7021        computed: true,
7022        digest: None,
7023    })))
7024}
7025
7026/// Create an AST node for `fixed([point, [x, y]])`.
7027fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7028    // Create [x, y] array literal.
7029    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7030        position.x,
7031    )?))));
7032    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7033        position.y,
7034    )?))));
7035    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7036        elements: vec![x_literal, y_literal],
7037        digest: None,
7038        non_code_meta: Default::default(),
7039    })));
7040
7041    // Create [point, [x, y]] outer array.
7042    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7043        elements: vec![point_expr, point_array],
7044        digest: None,
7045        non_code_meta: Default::default(),
7046    })));
7047
7048    // Create fixed([...])
7049    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
7050        ast::CallExpressionKw {
7051            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7052            unlabeled: Some(array_expr),
7053            arguments: Default::default(),
7054            digest: None,
7055            non_code_meta: Default::default(),
7056        },
7057    ))))
7058}
7059
7060/// Create an AST node for equalLength([line1, line2, ...])
7061pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7062    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7063        elements: line_exprs,
7064        digest: None,
7065        non_code_meta: Default::default(),
7066    })));
7067
7068    // Create equalLength([...])
7069    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7070        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7071        unlabeled: Some(array_expr),
7072        arguments: Default::default(),
7073        digest: None,
7074        non_code_meta: Default::default(),
7075    })))
7076}
7077
7078/// Create an AST node for equalRadius([seg1, seg2, ...])
7079pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7080    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7081        elements: segment_exprs,
7082        digest: None,
7083        non_code_meta: Default::default(),
7084    })));
7085
7086    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7087        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7088        unlabeled: Some(array_expr),
7089        arguments: Default::default(),
7090        digest: None,
7091        non_code_meta: Default::default(),
7092    })))
7093}
7094
7095/// Create an AST node for tangent([seg1, seg2])
7096pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7097    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7098        elements: vec![seg1_expr, seg2_expr],
7099        digest: None,
7100        non_code_meta: Default::default(),
7101    })));
7102
7103    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7104        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7105        unlabeled: Some(array_expr),
7106        arguments: Default::default(),
7107        digest: None,
7108        non_code_meta: Default::default(),
7109    })))
7110}
7111
7112/// Create an AST node for symmetric([input1, input2], axis = line)
7113pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7114    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
7115        elements: input_exprs,
7116        digest: None,
7117        non_code_meta: Default::default(),
7118    })));
7119    let arguments = vec![ast::LabeledArg {
7120        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7121        arg: axis_expr,
7122    }];
7123
7124    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7125        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7126        unlabeled: Some(array_expr),
7127        arguments,
7128        digest: None,
7129        non_code_meta: Default::default(),
7130    })))
7131}
7132
7133/// Create an AST node for midpoint(segment, point = point)
7134pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7135    let arguments = vec![ast::LabeledArg {
7136        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7137        arg: point_expr,
7138    }];
7139
7140    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7141        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7142        unlabeled: Some(segment_expr),
7143        arguments,
7144        digest: None,
7145        non_code_meta: Default::default(),
7146    })))
7147}
7148
7149#[cfg(test)]
7150mod tests {
7151    use std::sync;
7152
7153    use super::*;
7154    use crate::engine::PlaneName;
7155    use crate::engine::engine_manager::EngineManager;
7156    use crate::execution::cache::SketchModeState;
7157    use crate::execution::cache::clear_mem_cache;
7158    use crate::execution::cache::read_old_memory;
7159    use crate::execution::cache::write_old_memory;
7160    use crate::front::Distance;
7161    use crate::front::Fixed;
7162    use crate::front::FixedPoint;
7163    use crate::front::Midpoint;
7164    use crate::front::Object;
7165    use crate::front::Plane;
7166    use crate::front::Sketch;
7167    use crate::front::Tangent;
7168    use crate::frontend::sketch::Vertical;
7169    use crate::pretty::NumericSuffix;
7170
7171    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7172        for object in &scene_graph.objects {
7173            if let ObjectKind::Sketch(_) = &object.kind {
7174                return Some(object);
7175            }
7176        }
7177        None
7178    }
7179
7180    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7181        for object in &scene_graph.objects {
7182            if let ObjectKind::Face(_) = &object.kind {
7183                return Some(object);
7184            }
7185        }
7186        None
7187    }
7188
7189    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7190        for object in &scene_graph.objects {
7191            if matches!(&object.kind, ObjectKind::Wall(_)) {
7192                return Some(object.id);
7193            }
7194        }
7195        None
7196    }
7197
7198    fn find_cap_object_id_with_solid_output_index(
7199        scene_graph: &SceneGraph,
7200        cap_kind: crate::frontend::api::CapKind,
7201        solid_output_index: usize,
7202    ) -> Option<ObjectId> {
7203        for object in &scene_graph.objects {
7204            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7205            {
7206                return Some(object.id);
7207            }
7208        }
7209        None
7210    }
7211
7212    #[test]
7213    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7214        let source = "\
7215region001 = region(point = [0.1, 0.1], sketch = s)
7216extrude001 = extrude(region001, length = 5)
7217revolve001 = revolve(region001, axis = Y)
7218sweep001 = sweep(region001, path = path001)
7219loft001 = loft(region001)
7220not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7221";
7222
7223        let program = Program::parse(source).unwrap().0.unwrap();
7224
7225        assert_eq!(
7226            region_name_from_sweep_variable(&program.ast, "extrude001"),
7227            Some("region001".to_owned())
7228        );
7229        assert_eq!(
7230            region_name_from_sweep_variable(&program.ast, "revolve001"),
7231            Some("region001".to_owned())
7232        );
7233        assert_eq!(
7234            region_name_from_sweep_variable(&program.ast, "sweep001"),
7235            Some("region001".to_owned())
7236        );
7237        assert_eq!(
7238            region_name_from_sweep_variable(&program.ast, "loft001"),
7239            Some("region001".to_owned())
7240        );
7241        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7242    }
7243
7244    #[track_caller]
7245    fn expect_sketch(object: &Object) -> &Sketch {
7246        if let ObjectKind::Sketch(sketch) = &object.kind {
7247            sketch
7248        } else {
7249            panic!("Object is not a sketch: {:?}", object);
7250        }
7251    }
7252
7253    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7254        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7255        let ObjectKind::Segment {
7256            segment: Segment::Point(point),
7257        } = &point_object.kind
7258        else {
7259            panic!("Object is not a point segment: {point_object:?}");
7260        };
7261        point.position.clone()
7262    }
7263
7264    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7265        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7266        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7267    }
7268
7269    /// Build a millimeter-valued point expression for concise sketch edit test
7270    /// setup.
7271    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7272        Point2d {
7273            x: Expr::Var(Number {
7274                value: x,
7275                units: NumericSuffix::Mm,
7276            }),
7277            y: Expr::Var(Number {
7278                value: y,
7279                units: NumericSuffix::Mm,
7280            }),
7281        }
7282    }
7283
7284    /// Build a millimeter-valued numeric point for comparing solved scene graph
7285    /// positions.
7286    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7287        Point2d {
7288            x: Number {
7289                value: x,
7290                units: NumericSuffix::Mm,
7291            },
7292            y: Number {
7293                value: y,
7294                units: NumericSuffix::Mm,
7295            },
7296        }
7297    }
7298
7299    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7300        LineCtor {
7301            start: Point2d {
7302                x: Expr::Number(Number { value: start_x, units }),
7303                y: Expr::Number(Number { value: start_y, units }),
7304            },
7305            end: Point2d {
7306                x: Expr::Number(Number { value: end_x, units }),
7307                y: Expr::Number(Number { value: end_y, units }),
7308            },
7309            construction: None,
7310        }
7311    }
7312
7313    async fn create_sketch_with_single_line(
7314        frontend: &mut FrontendState,
7315        ctx: &ExecutorContext,
7316        mock_ctx: &ExecutorContext,
7317        version: Version,
7318    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7319        frontend.program = Program::empty();
7320
7321        let sketch_args = SketchCtor {
7322            on: Plane::Default(PlaneName::Xy),
7323        };
7324        let (_src_delta, _scene_delta, sketch_id) = frontend
7325            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7326            .await
7327            .unwrap();
7328
7329        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7330        let (source_delta, scene_graph_delta) = frontend
7331            .add_segment(mock_ctx, version, sketch_id, segment, None)
7332            .await
7333            .unwrap();
7334        let line_id = *scene_graph_delta
7335            .new_objects
7336            .last()
7337            .expect("Expected line object id to be created");
7338
7339        (sketch_id, line_id, source_delta, scene_graph_delta)
7340    }
7341
7342    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7343        frontend.program = program.clone();
7344        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7345        frontend.update_state_after_exec(outcome, true);
7346    }
7347
7348    #[test]
7349    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7350        for (source, expected_message) in [
7351            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7352            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7353        ] {
7354            let err = parse_frontend_mutation_source(
7355                source,
7356                "Error parsing KCL source after editing",
7357                "No AST produced after editing",
7358            )
7359            .expect_err("expected invalid KCL source to fail");
7360            let message = err.error.message();
7361
7362            assert_eq!(message, expected_message);
7363            assert!(!message.contains("CompilationIssue"));
7364            assert!(!message.contains("KclErrorDetails"));
7365            assert!(!message.contains("source_range"));
7366        }
7367    }
7368
7369    #[tokio::test(flavor = "multi_thread")]
7370    async fn test_edit_constraint_parse_error_messages_are_user_facing() {
7371        let initial_source = "\
7372sketch(on = XY) {
7373  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7374  distance([line1.start, line1.end]) == 10
7375}
7376";
7377        let program = Program::parse(initial_source).unwrap().0.unwrap();
7378
7379        let mut frontend = FrontendState::new();
7380        let mock_ctx = ExecutorContext::new_mock(None).await;
7381        let version = Version(0);
7382
7383        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7384        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7385        let sketch_id = sketch_object.id;
7386        let sketch = expect_sketch(sketch_object);
7387        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7388
7389        for (value, expected_message) in [
7390            ("**", "Invalid constraint value: Unexpected token: *"),
7391            ("3'", "Invalid constraint value: found unknown token '''"),
7392        ] {
7393            let err = frontend
7394                .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7395                .await
7396                .expect_err("expected invalid constraint expression to fail");
7397            let message = err.error.message();
7398
7399            assert_eq!(message, expected_message);
7400            assert!(!message.contains("CompilationIssue"));
7401            assert!(!message.contains("KclErrorDetails"));
7402            assert!(!message.contains("source_range"));
7403        }
7404
7405        mock_ctx.close().await;
7406    }
7407
7408    #[tokio::test(flavor = "multi_thread")]
7409    async fn test_failed_edit_constraint_does_not_update_program() {
7410        let initial_source = "\
7411sketch(on = XY) {
7412  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7413  distance([line1.start, line1.end]) == 10
7414}
7415";
7416        let program = Program::parse(initial_source).unwrap().0.unwrap();
7417        let original_source = program.original_file_contents.clone();
7418
7419        let mut frontend = FrontendState::new();
7420        let mock_ctx = ExecutorContext::new_mock(None).await;
7421        let version = Version(0);
7422
7423        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7424        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7425        let sketch_id = sketch_object.id;
7426        let sketch = expect_sketch(sketch_object);
7427        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7428
7429        frontend
7430            .edit_constraint(
7431                &mock_ctx,
7432                version,
7433                sketch_id,
7434                constraint_id,
7435                "unknownDistance".to_owned(),
7436            )
7437            .await
7438            .expect_err("expected invalid constraint value to fail execution");
7439
7440        assert_eq!(frontend.program.original_file_contents, original_source);
7441        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7442
7443        mock_ctx.close().await;
7444    }
7445
7446    #[tokio::test(flavor = "multi_thread")]
7447    async fn test_edit_constraint_array_index_oob_fails_in_sketch_mode() {
7448        let initial_source = "\
7449arr = [0]
7450sketch(on = XY) {
7451  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7452  distance([line1.start, line1.end]) == 10
7453}
7454";
7455        let program = Program::parse(initial_source).unwrap().0.unwrap();
7456
7457        let mut frontend = FrontendState::new();
7458        let mock_ctx = ExecutorContext::new_mock(None).await;
7459        let version = Version(0);
7460
7461        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7462        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7463        let sketch_id = sketch_object.id;
7464        let sketch = expect_sketch(sketch_object);
7465        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7466
7467        // In sketch mode execution, an out-of-bounds array index is an error.
7468        // It should not fall back to the first element of the array the way
7469        // plain mock execution does.
7470        let err = frontend
7471            .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7472            .await
7473            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7474        let message = err.error.message();
7475        assert!(
7476            message.contains("The array doesn't have any item at index 5"),
7477            "unexpected error message: {message}"
7478        );
7479
7480        mock_ctx.close().await;
7481    }
7482
7483    #[tokio::test(flavor = "multi_thread")]
7484    async fn test_sketch_checkpoint_round_trip_restores_state() {
7485        let mut frontend = FrontendState::new();
7486        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7487        let mock_ctx = ExecutorContext::new_mock(None).await;
7488        let version = Version(0);
7489
7490        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7491            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7492
7493        let expected_source = source_delta.text.clone();
7494        let expected_scene_graph = frontend.scene_graph.clone();
7495        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7496        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7497
7498        let checkpoint_id = frontend
7499            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7500            .await
7501            .unwrap();
7502
7503        let edited_segments = vec![ExistingSegmentCtor {
7504            id: line_id,
7505            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7506        }];
7507        let (edited_source, _edited_scene) = frontend
7508            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7509            .await
7510            .unwrap();
7511        assert_ne!(edited_source.text, expected_source);
7512
7513        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7514
7515        assert_eq!(restored.source_delta.text, expected_source);
7516        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7517        assert!(restored.scene_graph_delta.invalidates_ids);
7518        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7519        assert_eq!(frontend.scene_graph, expected_scene_graph);
7520        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7521
7522        ctx.close().await;
7523    }
7524
7525    #[tokio::test(flavor = "multi_thread")]
7526    async fn test_sketch_checkpoints_prune_oldest_entries() {
7527        let mut frontend = FrontendState::new();
7528        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7529        let mock_ctx = ExecutorContext::new_mock(None).await;
7530        let version = Version(0);
7531
7532        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7533            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7534
7535        let mut checkpoint_ids = Vec::new();
7536        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7537            checkpoint_ids.push(
7538                frontend
7539                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7540                    .await
7541                    .unwrap(),
7542            );
7543        }
7544
7545        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7546        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7547
7548        let oldest_retained = checkpoint_ids[3];
7549        assert_eq!(
7550            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7551            Some(oldest_retained)
7552        );
7553
7554        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7555        assert!(evicted_restore.is_err());
7556        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7557
7558        frontend
7559            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7560            .await
7561            .unwrap();
7562
7563        ctx.close().await;
7564    }
7565
7566    #[tokio::test(flavor = "multi_thread")]
7567    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7568        let mut frontend = FrontendState::new();
7569        let missing_checkpoint = SketchCheckpointId::new(999);
7570
7571        let err = frontend
7572            .restore_sketch_checkpoint(missing_checkpoint)
7573            .await
7574            .expect_err("Expected restore to fail for missing checkpoint");
7575
7576        assert!(err.msg.contains("Sketch checkpoint not found"));
7577    }
7578
7579    #[tokio::test(flavor = "multi_thread")]
7580    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7581        let mut frontend = FrontendState::new();
7582        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7583        let mock_ctx = ExecutorContext::new_mock(None).await;
7584        let version = Version(0);
7585
7586        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7587            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7588
7589        let checkpoint_a = frontend
7590            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7591            .await
7592            .unwrap();
7593        let checkpoint_b = frontend
7594            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7595            .await
7596            .unwrap();
7597        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7598
7599        frontend.clear_sketch_checkpoints();
7600        assert!(frontend.sketch_checkpoints.is_empty());
7601        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7602        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7603
7604        ctx.close().await;
7605    }
7606
7607    #[tokio::test(flavor = "multi_thread")]
7608    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7609        let mut frontend = FrontendState::new();
7610        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7611        let mock_ctx = ExecutorContext::new_mock(None).await;
7612        let version = Version(0);
7613
7614        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7615            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7616        let old_source = source_delta.text.clone();
7617        let old_checkpoint = frontend
7618            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7619            .await
7620            .unwrap();
7621        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7622
7623        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7624            .unwrap()
7625            .0
7626            .unwrap();
7627
7628        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7629        let SetProgramOutcome::Success {
7630            checkpoint_id: Some(new_checkpoint),
7631            ..
7632        } = result
7633        else {
7634            panic!("Expected Success with a fresh checkpoint baseline");
7635        };
7636
7637        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7638
7639        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7640        assert_eq!(old_restore.source_delta.text, old_source);
7641
7642        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7643        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7644
7645        ctx.close().await;
7646    }
7647
7648    #[tokio::test(flavor = "multi_thread")]
7649    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7650        let mut frontend = FrontendState::new();
7651        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7652        let mock_ctx = ExecutorContext::new_mock(None).await;
7653        let version = Version(0);
7654
7655        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7656            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7657        let old_checkpoint = frontend
7658            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7659            .await
7660            .unwrap();
7661        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7662
7663        let failing_program = Program::parse(
7664            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7665        )
7666        .unwrap()
7667        .0
7668        .unwrap();
7669
7670        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7671        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7672        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7673        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7674
7675        ctx.close().await;
7676    }
7677
7678    #[tokio::test(flavor = "multi_thread")]
7679    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7680        let mut frontend = FrontendState::new();
7681        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7682
7683        let program = Program::parse(
7684            "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",
7685        )
7686        .unwrap()
7687        .0
7688        .unwrap();
7689        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7690        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7691            panic!("Expected successful baseline program execution");
7692        };
7693
7694        clear_mem_cache().await;
7695        assert!(read_old_memory().await.is_none());
7696
7697        let checkpoint_without_mock_memory = frontend
7698            .create_sketch_checkpoint((*exec_outcome).clone())
7699            .await
7700            .unwrap();
7701
7702        write_old_memory(SketchModeState::new_for_tests()).await;
7703        assert!(read_old_memory().await.is_some());
7704
7705        let checkpoint_with_mock_memory = frontend
7706            .create_sketch_checkpoint((*exec_outcome).clone())
7707            .await
7708            .unwrap();
7709
7710        clear_mem_cache().await;
7711        assert!(read_old_memory().await.is_none());
7712
7713        frontend
7714            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7715            .await
7716            .unwrap();
7717        assert!(read_old_memory().await.is_some());
7718
7719        frontend
7720            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7721            .await
7722            .unwrap();
7723        assert!(read_old_memory().await.is_none());
7724
7725        ctx.close().await;
7726    }
7727
7728    #[tokio::test(flavor = "multi_thread")]
7729    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7730        let source = "\
7731sketch(on = XY) {
7732  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7733}
7734
7735bad = missing_name
7736";
7737        let program = Program::parse(source).unwrap().0.unwrap();
7738
7739        let mut frontend = FrontendState::new();
7740
7741        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7742        let mock_ctx = ExecutorContext::new_mock(None).await;
7743        let version = Version(0);
7744        let project_id = ProjectId(0);
7745        let file_id = FileId(0);
7746
7747        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7748            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7749        };
7750
7751        let sketch_id = frontend
7752            .scene_graph
7753            .objects
7754            .iter()
7755            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7756            .expect("Expected sketch object from errored hack_set_program");
7757
7758        frontend
7759            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7760            .await
7761            .unwrap();
7762
7763        ctx.close().await;
7764        mock_ctx.close().await;
7765    }
7766
7767    #[tokio::test(flavor = "multi_thread")]
7768    async fn test_new_sketch_add_point_edit_point() {
7769        let program = Program::empty();
7770
7771        let mut frontend = FrontendState::new();
7772        frontend.program = program;
7773
7774        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7775        let mock_ctx = ExecutorContext::new_mock(None).await;
7776        let version = Version(0);
7777
7778        let sketch_args = SketchCtor {
7779            on: Plane::Default(PlaneName::Xy),
7780        };
7781        let (_src_delta, scene_delta, sketch_id) = frontend
7782            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7783            .await
7784            .unwrap();
7785        assert_eq!(sketch_id, ObjectId(1));
7786        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7787        let sketch_object = &scene_delta.new_graph.objects[1];
7788        assert_eq!(sketch_object.id, ObjectId(1));
7789        assert_eq!(
7790            sketch_object.kind,
7791            ObjectKind::Sketch(Sketch {
7792                args: SketchCtor {
7793                    on: Plane::Default(PlaneName::Xy)
7794                },
7795                plane: ObjectId(0),
7796                segments: vec![],
7797                constraints: vec![],
7798            })
7799        );
7800        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7801
7802        let point_ctor = PointCtor {
7803            position: Point2d {
7804                x: Expr::Number(Number {
7805                    value: 1.0,
7806                    units: NumericSuffix::Inch,
7807                }),
7808                y: Expr::Number(Number {
7809                    value: 2.0,
7810                    units: NumericSuffix::Inch,
7811                }),
7812            },
7813        };
7814        let segment = SegmentCtor::Point(point_ctor);
7815        let (src_delta, scene_delta) = frontend
7816            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7817            .await
7818            .unwrap();
7819        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
7820        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7821        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7822        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7823            assert_eq!(scene_object.id.0, i);
7824        }
7825
7826        let point_id = *scene_delta.new_objects.last().unwrap();
7827
7828        let point_ctor = PointCtor {
7829            position: Point2d {
7830                x: Expr::Number(Number {
7831                    value: 3.0,
7832                    units: NumericSuffix::Inch,
7833                }),
7834                y: Expr::Number(Number {
7835                    value: 4.0,
7836                    units: NumericSuffix::Inch,
7837                }),
7838            },
7839        };
7840        let segments = vec![ExistingSegmentCtor {
7841            id: point_id,
7842            ctor: SegmentCtor::Point(point_ctor),
7843        }];
7844        let (src_delta, scene_delta) = frontend
7845            .edit_segments(&mock_ctx, version, sketch_id, segments)
7846            .await
7847            .unwrap();
7848        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
7849        assert_eq!(scene_delta.new_objects, vec![]);
7850        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7851
7852        ctx.close().await;
7853        mock_ctx.close().await;
7854    }
7855
7856    #[tokio::test(flavor = "multi_thread")]
7857    async fn test_new_sketch_add_line_edit_line() {
7858        let program = Program::empty();
7859
7860        let mut frontend = FrontendState::new();
7861        frontend.program = program;
7862
7863        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7864        let mock_ctx = ExecutorContext::new_mock(None).await;
7865        let version = Version(0);
7866
7867        let sketch_args = SketchCtor {
7868            on: Plane::Default(PlaneName::Xy),
7869        };
7870        let (_src_delta, scene_delta, sketch_id) = frontend
7871            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7872            .await
7873            .unwrap();
7874        assert_eq!(sketch_id, ObjectId(1));
7875        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7876        let sketch_object = &scene_delta.new_graph.objects[1];
7877        assert_eq!(sketch_object.id, ObjectId(1));
7878        assert_eq!(
7879            sketch_object.kind,
7880            ObjectKind::Sketch(Sketch {
7881                args: SketchCtor {
7882                    on: Plane::Default(PlaneName::Xy)
7883                },
7884                plane: ObjectId(0),
7885                segments: vec![],
7886                constraints: vec![],
7887            })
7888        );
7889        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7890
7891        let line_ctor = LineCtor {
7892            start: Point2d {
7893                x: Expr::Number(Number {
7894                    value: 0.0,
7895                    units: NumericSuffix::Mm,
7896                }),
7897                y: Expr::Number(Number {
7898                    value: 0.0,
7899                    units: NumericSuffix::Mm,
7900                }),
7901            },
7902            end: Point2d {
7903                x: Expr::Number(Number {
7904                    value: 10.0,
7905                    units: NumericSuffix::Mm,
7906                }),
7907                y: Expr::Number(Number {
7908                    value: 10.0,
7909                    units: NumericSuffix::Mm,
7910                }),
7911            },
7912            construction: None,
7913        };
7914        let segment = SegmentCtor::Line(line_ctor);
7915        let (src_delta, scene_delta) = frontend
7916            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7917            .await
7918            .unwrap();
7919        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
7920        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7921        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7922        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7923            assert_eq!(scene_object.id.0, i);
7924        }
7925
7926        // The new objects are the end points and then the line.
7927        let line = *scene_delta.new_objects.last().unwrap();
7928
7929        let line_ctor = LineCtor {
7930            start: Point2d {
7931                x: Expr::Number(Number {
7932                    value: 1.0,
7933                    units: NumericSuffix::Mm,
7934                }),
7935                y: Expr::Number(Number {
7936                    value: 2.0,
7937                    units: NumericSuffix::Mm,
7938                }),
7939            },
7940            end: Point2d {
7941                x: Expr::Number(Number {
7942                    value: 13.0,
7943                    units: NumericSuffix::Mm,
7944                }),
7945                y: Expr::Number(Number {
7946                    value: 14.0,
7947                    units: NumericSuffix::Mm,
7948                }),
7949            },
7950            construction: None,
7951        };
7952        let segments = vec![ExistingSegmentCtor {
7953            id: line,
7954            ctor: SegmentCtor::Line(line_ctor),
7955        }];
7956        let (src_delta, scene_delta) = frontend
7957            .edit_segments(&mock_ctx, version, sketch_id, segments)
7958            .await
7959            .unwrap();
7960        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
7961        assert_eq!(scene_delta.new_objects, vec![]);
7962        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7963
7964        ctx.close().await;
7965        mock_ctx.close().await;
7966    }
7967
7968    #[tokio::test(flavor = "multi_thread")]
7969    async fn test_new_sketch_add_arc_edit_arc() {
7970        let program = Program::empty();
7971
7972        let mut frontend = FrontendState::new();
7973        frontend.program = program;
7974
7975        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7976        let mock_ctx = ExecutorContext::new_mock(None).await;
7977        let version = Version(0);
7978
7979        let sketch_args = SketchCtor {
7980            on: Plane::Default(PlaneName::Xy),
7981        };
7982        let (_src_delta, scene_delta, sketch_id) = frontend
7983            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7984            .await
7985            .unwrap();
7986        assert_eq!(sketch_id, ObjectId(1));
7987        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7988        let sketch_object = &scene_delta.new_graph.objects[1];
7989        assert_eq!(sketch_object.id, ObjectId(1));
7990        assert_eq!(
7991            sketch_object.kind,
7992            ObjectKind::Sketch(Sketch {
7993                args: SketchCtor {
7994                    on: Plane::Default(PlaneName::Xy),
7995                },
7996                plane: ObjectId(0),
7997                segments: vec![],
7998                constraints: vec![],
7999            })
8000        );
8001        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8002
8003        let arc_ctor = ArcCtor {
8004            start: Point2d {
8005                x: Expr::Var(Number {
8006                    value: 0.0,
8007                    units: NumericSuffix::Mm,
8008                }),
8009                y: Expr::Var(Number {
8010                    value: 0.0,
8011                    units: NumericSuffix::Mm,
8012                }),
8013            },
8014            end: Point2d {
8015                x: Expr::Var(Number {
8016                    value: 10.0,
8017                    units: NumericSuffix::Mm,
8018                }),
8019                y: Expr::Var(Number {
8020                    value: 10.0,
8021                    units: NumericSuffix::Mm,
8022                }),
8023            },
8024            center: Point2d {
8025                x: Expr::Var(Number {
8026                    value: 10.0,
8027                    units: NumericSuffix::Mm,
8028                }),
8029                y: Expr::Var(Number {
8030                    value: 0.0,
8031                    units: NumericSuffix::Mm,
8032                }),
8033            },
8034            construction: None,
8035        };
8036        let segment = SegmentCtor::Arc(arc_ctor);
8037        let (src_delta, scene_delta) = frontend
8038            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8039            .await
8040            .unwrap();
8041        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8042        assert_eq!(
8043            scene_delta.new_objects,
8044            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8045        );
8046        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8047            assert_eq!(scene_object.id.0, i);
8048        }
8049        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8050
8051        // The new objects are the end points, the center, and then the arc.
8052        let arc = *scene_delta.new_objects.last().unwrap();
8053
8054        let arc_ctor = ArcCtor {
8055            start: Point2d {
8056                x: Expr::Var(Number {
8057                    value: 1.0,
8058                    units: NumericSuffix::Mm,
8059                }),
8060                y: Expr::Var(Number {
8061                    value: 2.0,
8062                    units: NumericSuffix::Mm,
8063                }),
8064            },
8065            end: Point2d {
8066                x: Expr::Var(Number {
8067                    value: 13.0,
8068                    units: NumericSuffix::Mm,
8069                }),
8070                y: Expr::Var(Number {
8071                    value: 14.0,
8072                    units: NumericSuffix::Mm,
8073                }),
8074            },
8075            center: Point2d {
8076                x: Expr::Var(Number {
8077                    value: 13.0,
8078                    units: NumericSuffix::Mm,
8079                }),
8080                y: Expr::Var(Number {
8081                    value: 2.0,
8082                    units: NumericSuffix::Mm,
8083                }),
8084            },
8085            construction: None,
8086        };
8087        let segments = vec![ExistingSegmentCtor {
8088            id: arc,
8089            ctor: SegmentCtor::Arc(arc_ctor),
8090        }];
8091        let (src_delta, scene_delta) = frontend
8092            .edit_segments(&mock_ctx, version, sketch_id, segments)
8093            .await
8094            .unwrap();
8095        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8096        assert_eq!(scene_delta.new_objects, vec![]);
8097        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8098
8099        ctx.close().await;
8100        mock_ctx.close().await;
8101    }
8102
8103    #[tokio::test(flavor = "multi_thread")]
8104    async fn test_new_sketch_add_circle_edit_circle() {
8105        let program = Program::empty();
8106
8107        let mut frontend = FrontendState::new();
8108        frontend.program = program;
8109
8110        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8111        let mock_ctx = ExecutorContext::new_mock(None).await;
8112        let version = Version(0);
8113
8114        let sketch_args = SketchCtor {
8115            on: Plane::Default(PlaneName::Xy),
8116        };
8117        let (_src_delta, _scene_delta, sketch_id) = frontend
8118            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8119            .await
8120            .unwrap();
8121
8122        // Add a circle segment.
8123        let circle_ctor = CircleCtor {
8124            start: Point2d {
8125                x: Expr::Var(Number {
8126                    value: 5.0,
8127                    units: NumericSuffix::Mm,
8128                }),
8129                y: Expr::Var(Number {
8130                    value: 0.0,
8131                    units: NumericSuffix::Mm,
8132                }),
8133            },
8134            center: Point2d {
8135                x: Expr::Var(Number {
8136                    value: 0.0,
8137                    units: NumericSuffix::Mm,
8138                }),
8139                y: Expr::Var(Number {
8140                    value: 0.0,
8141                    units: NumericSuffix::Mm,
8142                }),
8143            },
8144            construction: None,
8145        };
8146        let segment = SegmentCtor::Circle(circle_ctor);
8147        let (src_delta, scene_delta) = frontend
8148            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8149            .await
8150            .unwrap();
8151        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8152        // The new objects are start, center, and then the circle segment.
8153        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8154        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8155
8156        let circle = *scene_delta.new_objects.last().unwrap();
8157
8158        // Edit the circle segment.
8159        let circle_ctor = CircleCtor {
8160            start: Point2d {
8161                x: Expr::Var(Number {
8162                    value: 10.0,
8163                    units: NumericSuffix::Mm,
8164                }),
8165                y: Expr::Var(Number {
8166                    value: 0.0,
8167                    units: NumericSuffix::Mm,
8168                }),
8169            },
8170            center: Point2d {
8171                x: Expr::Var(Number {
8172                    value: 3.0,
8173                    units: NumericSuffix::Mm,
8174                }),
8175                y: Expr::Var(Number {
8176                    value: 4.0,
8177                    units: NumericSuffix::Mm,
8178                }),
8179            },
8180            construction: None,
8181        };
8182        let segments = vec![ExistingSegmentCtor {
8183            id: circle,
8184            ctor: SegmentCtor::Circle(circle_ctor),
8185        }];
8186        let (src_delta, scene_delta) = frontend
8187            .edit_segments(&mock_ctx, version, sketch_id, segments)
8188            .await
8189            .unwrap();
8190        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8191        assert_eq!(scene_delta.new_objects, vec![]);
8192        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8193
8194        ctx.close().await;
8195        mock_ctx.close().await;
8196    }
8197
8198    #[tokio::test(flavor = "multi_thread")]
8199    async fn test_delete_circle() {
8200        let initial_source = "sketch001 = sketch(on = XY) {
8201  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8202}
8203";
8204
8205        let program = Program::parse(initial_source).unwrap().0.unwrap();
8206        let mut frontend = FrontendState::new();
8207
8208        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8209        let mock_ctx = ExecutorContext::new_mock(None).await;
8210        let version = Version(0);
8211
8212        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8213        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8214        let sketch_id = sketch_object.id;
8215        let sketch = expect_sketch(sketch_object);
8216
8217        // The sketch should have 3 segments: start point, center point, and the circle.
8218        assert_eq!(sketch.segments.len(), 3);
8219        let circle_id = sketch.segments[2];
8220
8221        // Delete the circle.
8222        let (src_delta, scene_delta) = frontend
8223            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8224            .await
8225            .unwrap();
8226        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8227        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8228        let new_sketch = expect_sketch(new_sketch_object);
8229        assert_eq!(new_sketch.segments.len(), 0);
8230
8231        ctx.close().await;
8232        mock_ctx.close().await;
8233    }
8234
8235    #[tokio::test(flavor = "multi_thread")]
8236    async fn test_edit_circle_via_point() {
8237        let initial_source = "sketch001 = sketch(on = XY) {
8238  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8239}
8240";
8241
8242        let program = Program::parse(initial_source).unwrap().0.unwrap();
8243        let mut frontend = FrontendState::new();
8244
8245        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8246        let mock_ctx = ExecutorContext::new_mock(None).await;
8247        let version = Version(0);
8248
8249        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8250        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8251        let sketch_id = sketch_object.id;
8252        let sketch = expect_sketch(sketch_object);
8253
8254        // Find the circle segment and its start point.
8255        let circle_id = sketch
8256            .segments
8257            .iter()
8258            .copied()
8259            .find(|seg_id| {
8260                matches!(
8261                    &frontend.scene_graph.objects[seg_id.0].kind,
8262                    ObjectKind::Segment {
8263                        segment: Segment::Circle(_)
8264                    }
8265                )
8266            })
8267            .expect("Expected a circle segment in sketch");
8268        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8269        let ObjectKind::Segment {
8270            segment: Segment::Circle(circle),
8271        } = &circle_object.kind
8272        else {
8273            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8274        };
8275        let start_point_id = circle.start;
8276
8277        // Edit the start point via SegmentCtor::Point.
8278        let segments = vec![ExistingSegmentCtor {
8279            id: start_point_id,
8280            ctor: SegmentCtor::Point(PointCtor {
8281                position: Point2d {
8282                    x: Expr::Var(Number {
8283                        value: 7.0,
8284                        units: NumericSuffix::Mm,
8285                    }),
8286                    y: Expr::Var(Number {
8287                        value: 1.0,
8288                        units: NumericSuffix::Mm,
8289                    }),
8290                },
8291            }),
8292        }];
8293        let (src_delta, _scene_delta) = frontend
8294            .edit_segments(&mock_ctx, version, sketch_id, segments)
8295            .await
8296            .unwrap();
8297        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8298
8299        ctx.close().await;
8300        mock_ctx.close().await;
8301    }
8302
8303    #[tokio::test(flavor = "multi_thread")]
8304    async fn test_add_line_when_sketch_block_uses_variable() {
8305        let initial_source = "s = sketch(on = XY) {}
8306";
8307
8308        let program = Program::parse(initial_source).unwrap().0.unwrap();
8309
8310        let mut frontend = FrontendState::new();
8311
8312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8313        let mock_ctx = ExecutorContext::new_mock(None).await;
8314        let version = Version(0);
8315
8316        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8317        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8318        let sketch_id = sketch_object.id;
8319
8320        let line_ctor = LineCtor {
8321            start: Point2d {
8322                x: Expr::Number(Number {
8323                    value: 0.0,
8324                    units: NumericSuffix::Mm,
8325                }),
8326                y: Expr::Number(Number {
8327                    value: 0.0,
8328                    units: NumericSuffix::Mm,
8329                }),
8330            },
8331            end: Point2d {
8332                x: Expr::Number(Number {
8333                    value: 10.0,
8334                    units: NumericSuffix::Mm,
8335                }),
8336                y: Expr::Number(Number {
8337                    value: 10.0,
8338                    units: NumericSuffix::Mm,
8339                }),
8340            },
8341            construction: None,
8342        };
8343        let segment = SegmentCtor::Line(line_ctor);
8344        let (src_delta, scene_delta) = frontend
8345            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8346            .await
8347            .unwrap();
8348        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8349        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8350        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8351
8352        ctx.close().await;
8353        mock_ctx.close().await;
8354    }
8355
8356    #[tokio::test(flavor = "multi_thread")]
8357    async fn test_new_sketch_add_line_delete_sketch() {
8358        let program = Program::empty();
8359
8360        let mut frontend = FrontendState::new();
8361        frontend.program = program;
8362
8363        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8364        let mock_ctx = ExecutorContext::new_mock(None).await;
8365        let version = Version(0);
8366
8367        let sketch_args = SketchCtor {
8368            on: Plane::Default(PlaneName::Xy),
8369        };
8370        let (_src_delta, scene_delta, sketch_id) = frontend
8371            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8372            .await
8373            .unwrap();
8374        assert_eq!(sketch_id, ObjectId(1));
8375        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8376        let sketch_object = &scene_delta.new_graph.objects[1];
8377        assert_eq!(sketch_object.id, ObjectId(1));
8378        assert_eq!(
8379            sketch_object.kind,
8380            ObjectKind::Sketch(Sketch {
8381                args: SketchCtor {
8382                    on: Plane::Default(PlaneName::Xy)
8383                },
8384                plane: ObjectId(0),
8385                segments: vec![],
8386                constraints: vec![],
8387            })
8388        );
8389        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8390
8391        let line_ctor = LineCtor {
8392            start: Point2d {
8393                x: Expr::Number(Number {
8394                    value: 0.0,
8395                    units: NumericSuffix::Mm,
8396                }),
8397                y: Expr::Number(Number {
8398                    value: 0.0,
8399                    units: NumericSuffix::Mm,
8400                }),
8401            },
8402            end: Point2d {
8403                x: Expr::Number(Number {
8404                    value: 10.0,
8405                    units: NumericSuffix::Mm,
8406                }),
8407                y: Expr::Number(Number {
8408                    value: 10.0,
8409                    units: NumericSuffix::Mm,
8410                }),
8411            },
8412            construction: None,
8413        };
8414        let segment = SegmentCtor::Line(line_ctor);
8415        let (src_delta, scene_delta) = frontend
8416            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8417            .await
8418            .unwrap();
8419        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8420        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8421
8422        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8423        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8424        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8425
8426        ctx.close().await;
8427        mock_ctx.close().await;
8428    }
8429
8430    #[tokio::test(flavor = "multi_thread")]
8431    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8432        let initial_source = "s = sketch(on = XY) {}
8433";
8434
8435        let program = Program::parse(initial_source).unwrap().0.unwrap();
8436
8437        let mut frontend = FrontendState::new();
8438
8439        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8440        let version = Version(0);
8441
8442        frontend.hack_set_program(&ctx, program).await.unwrap();
8443        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8444        let sketch_id = sketch_object.id;
8445
8446        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8447        insta::assert_snapshot!(
8448            "test_delete_sketch_when_sketch_block_uses_variable",
8449            src_delta.text.as_str()
8450        );
8451        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8452
8453        ctx.close().await;
8454    }
8455
8456    #[tokio::test(flavor = "multi_thread")]
8457    async fn test_delete_sketch_after_comment() {
8458        let initial_source = "sketch001 = sketch(on = XZ) {
8459}
8460";
8461
8462        let program = Program::parse(initial_source).unwrap().0.unwrap();
8463        let mut frontend = FrontendState::new();
8464
8465        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8466        let version = Version(0);
8467
8468        frontend.hack_set_program(&ctx, program).await.unwrap();
8469        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8470        let sketch_id = sketch_object.id;
8471        let original_source = sketch_object.source.clone();
8472
8473        let commented_source = "// test 1
8474sketch001 = sketch(on = XZ) {
8475}
8476";
8477        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8478        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8479
8480        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8481        assert_eq!(cached_sketch_object.source, original_source);
8482
8483        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8484        assert!(
8485            !src_delta.text.contains("sketch001"),
8486            "sketch was not deleted: {}",
8487            src_delta.text
8488        );
8489        // The leading line comment must survive deletion.
8490        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8491        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8492
8493        ctx.close().await;
8494    }
8495
8496    #[tokio::test(flavor = "multi_thread")]
8497    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8498        let initial_source = "sketch001 = sketch(on = XZ) {
8499}
8500foo = 1
8501";
8502
8503        let program = Program::parse(initial_source).unwrap().0.unwrap();
8504        let mut frontend = FrontendState::new();
8505
8506        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8507        let version = Version(0);
8508
8509        frontend.hack_set_program(&ctx, program).await.unwrap();
8510        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8511        let sketch_id = sketch_object.id;
8512
8513        let commented_source = "// keep me
8514sketch001 = sketch(on = XZ) {
8515}
8516foo = 1
8517";
8518        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8519        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8520
8521        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8522        // The leading comment should remain, now attached to the following body item.
8523        insta::assert_snapshot!(
8524            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8525            src_delta.text.as_str()
8526        );
8527
8528        ctx.close().await;
8529    }
8530
8531    #[tokio::test(flavor = "multi_thread")]
8532    async fn test_delete_segment_preserves_pre_comment() {
8533        let initial_source = "\
8534sketch(on = XY) {
8535  point(at = [var 1, var 2])
8536  // describe the middle point
8537  point(at = [var 3, var 4])
8538  point(at = [var 5, var 6])
8539}
8540";
8541
8542        let program = Program::parse(initial_source).unwrap().0.unwrap();
8543        let mut frontend = FrontendState::new();
8544
8545        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8546        let mock_ctx = ExecutorContext::new_mock(None).await;
8547        let version = Version(0);
8548
8549        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8550        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8551        let sketch_id = sketch_object.id;
8552        let sketch = expect_sketch(sketch_object);
8553
8554        let middle_point_id = *sketch.segments.get(1).unwrap();
8555
8556        let (src_delta, _scene_delta) = frontend
8557            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8558            .await
8559            .unwrap();
8560        // The line comment on the line above the deleted point must be preserved.
8561        // It is reattached to the next surviving body item.
8562        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8563
8564        ctx.close().await;
8565        mock_ctx.close().await;
8566    }
8567
8568    #[tokio::test(flavor = "multi_thread")]
8569    async fn test_delete_last_segment_preserves_pre_comment() {
8570        let initial_source = "\
8571sketch(on = XY) {
8572  point(at = [var 1, var 2])
8573  // describe the trailing point
8574  point(at = [var 3, var 4])
8575}
8576";
8577
8578        let program = Program::parse(initial_source).unwrap().0.unwrap();
8579        let mut frontend = FrontendState::new();
8580
8581        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8582        let mock_ctx = ExecutorContext::new_mock(None).await;
8583        let version = Version(0);
8584
8585        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8586        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8587        let sketch_id = sketch_object.id;
8588        let sketch = expect_sketch(sketch_object);
8589
8590        let last_point_id = *sketch.segments.last().unwrap();
8591
8592        let (src_delta, _scene_delta) = frontend
8593            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8594            .await
8595            .unwrap();
8596        // No following item to attach to; the comment is kept inside the sketch
8597        // block as trailing non-code metadata so the user does not lose it.
8598        insta::assert_snapshot!(
8599            "test_delete_last_segment_preserves_pre_comment",
8600            src_delta.text.as_str()
8601        );
8602
8603        ctx.close().await;
8604        mock_ctx.close().await;
8605    }
8606
8607    #[tokio::test(flavor = "multi_thread")]
8608    async fn test_delete_segment_drops_inline_trailing_comment() {
8609        let initial_source = "\
8610sketch(on = XY) {
8611  point(at = [var 1, var 2])
8612  point(at = [var 3, var 4]) // same-line note that gets dropped
8613  point(at = [var 5, var 6])
8614}
8615";
8616
8617        let program = Program::parse(initial_source).unwrap().0.unwrap();
8618        let mut frontend = FrontendState::new();
8619
8620        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8621        let mock_ctx = ExecutorContext::new_mock(None).await;
8622        let version = Version(0);
8623
8624        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8625        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8626        let sketch_id = sketch_object.id;
8627        let sketch = expect_sketch(sketch_object);
8628
8629        let middle_point_id = *sketch.segments.get(1).unwrap();
8630
8631        let (src_delta, _scene_delta) = frontend
8632            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8633            .await
8634            .unwrap();
8635        // The same-line trailing comment is removed along with the deleted code.
8636        assert!(
8637            !src_delta.text.contains("same-line note"),
8638            "inline comment should have been removed: {}",
8639            src_delta.text
8640        );
8641
8642        ctx.close().await;
8643        mock_ctx.close().await;
8644    }
8645
8646    #[tokio::test(flavor = "multi_thread")]
8647    async fn test_delete_segments_preserves_block_comments_across_positions() {
8648        // One test exercising several `delete_body_item_preserving_pre_comments`
8649        // branches at once with `/* ... */` block comments:
8650        //   - first point: leading block comment must migrate to the next item.
8651        //   - first point: same-line trailing block comment must be dropped.
8652        //   - middle point: leading block comment must stay attached after migration.
8653        //   - last point: leading block comment, with no surviving next item,
8654        //     must be converted into a trailing NonCodeNode.
8655        let initial_source = "\
8656sketch(on = XY) {
8657  /* above first - moves to middle */
8658  point(at = [var 1, var 2]) /* same-line on first - dropped */
8659  /* above middle - stays */
8660  point(at = [var 3, var 4])
8661  /* above last - moves to trailing meta */
8662  point(at = [var 5, var 6])
8663}
8664";
8665
8666        let program = Program::parse(initial_source).unwrap().0.unwrap();
8667        let mut frontend = FrontendState::new();
8668
8669        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8670        let mock_ctx = ExecutorContext::new_mock(None).await;
8671        let version = Version(0);
8672
8673        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8674        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8675        let sketch_id = sketch_object.id;
8676        let sketch = expect_sketch(sketch_object);
8677
8678        let first_point_id = *sketch.segments.first().unwrap();
8679        let last_point_id = *sketch.segments.last().unwrap();
8680
8681        let (src_delta, _scene_delta) = frontend
8682            .delete_objects(
8683                &mock_ctx,
8684                version,
8685                sketch_id,
8686                Vec::new(),
8687                vec![first_point_id, last_point_id],
8688            )
8689            .await
8690            .unwrap();
8691        insta::assert_snapshot!(
8692            "test_delete_segments_preserves_block_comments_across_positions",
8693            src_delta.text.as_str()
8694        );
8695
8696        ctx.close().await;
8697        mock_ctx.close().await;
8698    }
8699
8700    #[tokio::test(flavor = "multi_thread")]
8701    async fn test_edit_line_when_editing_its_start_point() {
8702        let initial_source = "\
8703sketch(on = XY) {
8704  line(start = [var 1, var 2], end = [var 3, var 4])
8705}
8706";
8707
8708        let program = Program::parse(initial_source).unwrap().0.unwrap();
8709
8710        let mut frontend = FrontendState::new();
8711
8712        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8713        let mock_ctx = ExecutorContext::new_mock(None).await;
8714        let version = Version(0);
8715
8716        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8717        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8718        let sketch_id = sketch_object.id;
8719        let sketch = expect_sketch(sketch_object);
8720
8721        let point_id = *sketch.segments.first().unwrap();
8722
8723        let point_ctor = PointCtor {
8724            position: Point2d {
8725                x: Expr::Var(Number {
8726                    value: 5.0,
8727                    units: NumericSuffix::Inch,
8728                }),
8729                y: Expr::Var(Number {
8730                    value: 6.0,
8731                    units: NumericSuffix::Inch,
8732                }),
8733            },
8734        };
8735        let segments = vec![ExistingSegmentCtor {
8736            id: point_id,
8737            ctor: SegmentCtor::Point(point_ctor),
8738        }];
8739        let (src_delta, scene_delta) = frontend
8740            .edit_segments(&mock_ctx, version, sketch_id, segments)
8741            .await
8742            .unwrap();
8743        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8744        assert_eq!(scene_delta.new_objects, vec![]);
8745        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8746
8747        ctx.close().await;
8748        mock_ctx.close().await;
8749    }
8750
8751    #[tokio::test(flavor = "multi_thread")]
8752    async fn test_edit_line_when_editing_its_end_point() {
8753        let initial_source = "\
8754sketch(on = XY) {
8755  line(start = [var 1, var 2], end = [var 3, var 4])
8756}
8757";
8758
8759        let program = Program::parse(initial_source).unwrap().0.unwrap();
8760
8761        let mut frontend = FrontendState::new();
8762
8763        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8764        let mock_ctx = ExecutorContext::new_mock(None).await;
8765        let version = Version(0);
8766
8767        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8768        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8769        let sketch_id = sketch_object.id;
8770        let sketch = expect_sketch(sketch_object);
8771        let point_id = *sketch.segments.get(1).unwrap();
8772
8773        let point_ctor = PointCtor {
8774            position: Point2d {
8775                x: Expr::Var(Number {
8776                    value: 5.0,
8777                    units: NumericSuffix::Inch,
8778                }),
8779                y: Expr::Var(Number {
8780                    value: 6.0,
8781                    units: NumericSuffix::Inch,
8782                }),
8783            },
8784        };
8785        let segments = vec![ExistingSegmentCtor {
8786            id: point_id,
8787            ctor: SegmentCtor::Point(point_ctor),
8788        }];
8789        let (src_delta, scene_delta) = frontend
8790            .edit_segments(&mock_ctx, version, sketch_id, segments)
8791            .await
8792            .unwrap();
8793        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
8794        assert_eq!(scene_delta.new_objects, vec![]);
8795        assert_eq!(
8796            scene_delta.new_graph.objects.len(),
8797            5,
8798            "{:#?}",
8799            scene_delta.new_graph.objects
8800        );
8801
8802        ctx.close().await;
8803        mock_ctx.close().await;
8804    }
8805
8806    #[tokio::test(flavor = "multi_thread")]
8807    async fn test_edit_line_with_coincident_feedback() {
8808        let initial_source = "\
8809sketch(on = XY) {
8810  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8811  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8812  fixed([line1.start, [0, 0]])
8813  coincident([line1.end, line2.start])
8814  equalLength([line1, line2])
8815}
8816";
8817
8818        let program = Program::parse(initial_source).unwrap().0.unwrap();
8819
8820        let mut frontend = FrontendState::new();
8821
8822        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8823        let mock_ctx = ExecutorContext::new_mock(None).await;
8824        let version = Version(0);
8825
8826        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8827        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8828        let sketch_id = sketch_object.id;
8829        let sketch = expect_sketch(sketch_object);
8830        let line2_end_id = *sketch.segments.get(4).unwrap();
8831
8832        let segments = vec![ExistingSegmentCtor {
8833            id: line2_end_id,
8834            ctor: SegmentCtor::Point(PointCtor {
8835                position: Point2d {
8836                    x: Expr::Var(Number {
8837                        value: 9.0,
8838                        units: NumericSuffix::None,
8839                    }),
8840                    y: Expr::Var(Number {
8841                        value: 10.0,
8842                        units: NumericSuffix::None,
8843                    }),
8844                },
8845            }),
8846        }];
8847        let (src_delta, scene_delta) = frontend
8848            .edit_segments(&mock_ctx, version, sketch_id, segments)
8849            .await
8850            .unwrap();
8851        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
8852        assert_eq!(
8853            scene_delta.new_graph.objects.len(),
8854            11,
8855            "{:#?}",
8856            scene_delta.new_graph.objects
8857        );
8858
8859        ctx.close().await;
8860        mock_ctx.close().await;
8861    }
8862
8863    #[tokio::test(flavor = "multi_thread")]
8864    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8865        let initial_source = "\
8866sketch(on = XY) {
8867  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8868  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8869  fixed([line1.start, [0, 0]])
8870  coincident([line1.end, line2.start])
8871  equalLength([line1, line2])
8872}
8873";
8874
8875        let program = Program::parse(initial_source).unwrap().0.unwrap();
8876        let mut frontend = FrontendState::new();
8877        let mock_ctx = ExecutorContext::new_mock(None).await;
8878        let version = Version(0);
8879
8880        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8881        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8882        let sketch_id = sketch_object.id;
8883        let sketch = expect_sketch(sketch_object);
8884        let line2_end_id = *sketch.segments.get(4).unwrap();
8885
8886        let segments = vec![ExistingSegmentCtor {
8887            id: line2_end_id,
8888            ctor: SegmentCtor::Point(PointCtor {
8889                position: Point2d {
8890                    x: Expr::Var(Number {
8891                        value: 9.0,
8892                        units: NumericSuffix::None,
8893                    }),
8894                    y: Expr::Var(Number {
8895                        value: 10.0,
8896                        units: NumericSuffix::None,
8897                    }),
8898                },
8899            }),
8900        }];
8901        let (edited_source, _) = frontend
8902            .edit_segments(&mock_ctx, version, sketch_id, segments)
8903            .await
8904            .unwrap();
8905
8906        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8907        assert_eq!(mock_source.text, edited_source.text);
8908
8909        mock_ctx.close().await;
8910    }
8911
8912    /// Preview segment edits should return solved geometry without persisting
8913    /// solver feedback to KCL.
8914    #[tokio::test(flavor = "multi_thread")]
8915    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8916        let initial_source = "\
8917sketch(on = XY) {
8918  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8919  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8920  fixed([line1.start, [0, 0]])
8921  coincident([line1.end, line2.start])
8922  equalLength([line1, line2])
8923}
8924";
8925
8926        let program = Program::parse(initial_source).unwrap().0.unwrap();
8927        let mut frontend = FrontendState::new();
8928        let mock_ctx = ExecutorContext::new_mock(None).await;
8929        let version = Version(0);
8930
8931        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8932        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8933        let sketch_id = sketch_object.id;
8934        let sketch = expect_sketch(sketch_object);
8935        let line2_end_id = *sketch.segments.get(4).unwrap();
8936
8937        let segments = vec![ExistingSegmentCtor {
8938            id: line2_end_id,
8939            ctor: SegmentCtor::Point(PointCtor {
8940                position: Point2d {
8941                    x: Expr::Var(Number {
8942                        value: 9.0,
8943                        units: NumericSuffix::None,
8944                    }),
8945                    y: Expr::Var(Number {
8946                        value: 10.0,
8947                        units: NumericSuffix::None,
8948                    }),
8949                },
8950            }),
8951        }];
8952        let (preview_source, preview_delta) = frontend
8953            .edit_segments_with_options(
8954                &mock_ctx,
8955                version,
8956                sketch_id,
8957                segments,
8958                EditSegmentsOptions {
8959                    anchor_segment_ids: Some(vec![line2_end_id]),
8960                    drag_anchors: Vec::new(),
8961                    constraint_label_edits: Vec::new(),
8962                    commit_solved_initial_guesses: false,
8963                },
8964            )
8965            .await
8966            .unwrap();
8967
8968        assert!(
8969            !preview_delta.exec_outcome.var_solutions.is_empty(),
8970            "preview solve should still solve and return geometry feedback"
8971        );
8972        assert!(
8973            preview_source
8974                .text
8975                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8976        );
8977        assert!(
8978            preview_source
8979                .text
8980                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8981        );
8982
8983        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8984        assert_eq!(mock_source.text, preview_source.text);
8985
8986        mock_ctx.close().await;
8987    }
8988
8989    #[tokio::test(flavor = "multi_thread")]
8990    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8991        let initial_source = "\
8992sketch(on = XY) {
8993  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8994}
8995";
8996
8997        let program = Program::parse(initial_source).unwrap().0.unwrap();
8998        let mut frontend = FrontendState::new();
8999        let mock_ctx = ExecutorContext::new_mock(None).await;
9000        let version = Version(0);
9001
9002        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9003        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9004        let sketch_id = sketch_object.id;
9005        let sketch = expect_sketch(sketch_object);
9006        let line_end_id = *sketch.segments.get(1).unwrap();
9007
9008        let constraint = Constraint::Fixed(Fixed {
9009            points: vec![FixedPoint {
9010                point: line_end_id,
9011                position: Point2d {
9012                    x: Number {
9013                        value: 20.0,
9014                        units: NumericSuffix::Mm,
9015                    },
9016                    y: Number {
9017                        value: 0.0,
9018                        units: NumericSuffix::Mm,
9019                    },
9020                },
9021            }],
9022        });
9023        let (constraint_source, _) = frontend
9024            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9025            .await
9026            .unwrap();
9027
9028        assert!(
9029            constraint_source
9030                .text
9031                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9032            "{}",
9033            constraint_source.text
9034        );
9035        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9036        assert_eq!(mock_source.text, constraint_source.text);
9037
9038        mock_ctx.close().await;
9039    }
9040
9041    #[test]
9042    fn test_no_solver_feedback_preserves_original_source() {
9043        let initial_source = "\
9044@settings(defaultLengthUnit = in, kclVersion = 2.0)
9045cylinder = startSketchOn(XY)
9046    |> circle(center= [0, 0], radius= 22)
9047    |> extrude(length = 14)
9048";
9049        let mut frontend = FrontendState::new();
9050        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9051        let outcome = ExecOutcome {
9052            variables: Default::default(),
9053            operations: Default::default(),
9054            artifact_graph: Default::default(),
9055            scene_objects: Default::default(),
9056            source_range_to_object: Default::default(),
9057            var_solutions: Default::default(),
9058            refactor_metadata: Default::default(),
9059            issues: Default::default(),
9060            filenames: Default::default(),
9061            default_planes: Default::default(),
9062        };
9063
9064        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9065
9066        assert_eq!(source_delta.text, initial_source);
9067    }
9068
9069    /// Explicit drag anchors should limit which edited points become temporary
9070    /// fixed constraints.
9071    #[tokio::test(flavor = "multi_thread")]
9072    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9073        let initial_source = "\
9074sketch(on = XY) {
9075  point1 = point(at = [var 0mm, var 0mm])
9076  point2 = point(at = [var 0mm, var 0mm])
9077  coincident([point1, point2])
9078}
9079";
9080
9081        let program = Program::parse(initial_source).unwrap().0.unwrap();
9082        let mut frontend = FrontendState::new();
9083        let mock_ctx = ExecutorContext::new_mock(None).await;
9084        let version = Version(0);
9085
9086        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9088        let sketch_id = sketch_object.id;
9089        let sketch = expect_sketch(sketch_object);
9090        let point1_id = sketch.segments[0];
9091        let point2_id = sketch.segments[1];
9092
9093        let segments = vec![
9094            ExistingSegmentCtor {
9095                id: point1_id,
9096                ctor: SegmentCtor::Point(PointCtor {
9097                    position: point_expr_mm(10.0, 0.0),
9098                }),
9099            },
9100            ExistingSegmentCtor {
9101                id: point2_id,
9102                ctor: SegmentCtor::Point(PointCtor {
9103                    position: point_expr_mm(100.0, 0.0),
9104                }),
9105            },
9106        ];
9107        let (_, scene_delta) = frontend
9108            .edit_segments_with_options(
9109                &mock_ctx,
9110                version,
9111                sketch_id,
9112                segments,
9113                EditSegmentsOptions {
9114                    anchor_segment_ids: Some(vec![point1_id]),
9115                    drag_anchors: Vec::new(),
9116                    constraint_label_edits: Vec::new(),
9117                    commit_solved_initial_guesses: true,
9118                },
9119            )
9120            .await
9121            .unwrap();
9122
9123        assert_point_position_close(
9124            point_position(&scene_delta.new_graph, point1_id),
9125            point_number_mm(10.0, 0.0),
9126        );
9127        assert_point_position_close(
9128            point_position(&scene_delta.new_graph, point2_id),
9129            point_number_mm(10.0, 0.0),
9130        );
9131
9132        mock_ctx.close().await;
9133    }
9134
9135    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9136    /// for every SketchVar whose initial NumericLiteral has the given value.
9137    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9138        use std::cell::RefCell;
9139        struct Collector {
9140            target: f64,
9141            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9142        }
9143        impl<'a> crate::walk::Visitor<'a> for &Collector {
9144            type Error = crate::front::Error;
9145            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9146                if let crate::walk::Node::SketchVar(sketch_var) = node
9147                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9148                    && (initial.value - self.target).abs() < 1e-9
9149                {
9150                    self.out
9151                        .borrow_mut()
9152                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9153                }
9154                for child in node.children().iter() {
9155                    if !child.visit(*self)? {
9156                        return Ok(false);
9157                    }
9158                }
9159                Ok(true)
9160            }
9161        }
9162        let collector = Collector {
9163            target: value,
9164            out: Default::default(),
9165        };
9166        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9167        collector.out.into_inner()
9168    }
9169
9170    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9171    /// for every SketchVar (including bare `var`).
9172    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9173        use std::cell::RefCell;
9174        struct Collector {
9175            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9176        }
9177        impl<'a> crate::walk::Visitor<'a> for &Collector {
9178            type Error = crate::front::Error;
9179            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9180                if let crate::walk::Node::SketchVar(sketch_var) = node
9181                    && let Some(node_path) = &sketch_var.node_path
9182                {
9183                    self.out
9184                        .borrow_mut()
9185                        .push((SourceRange::from(sketch_var), node_path.clone()));
9186                }
9187                for child in node.children().iter() {
9188                    if !child.visit(*self)? {
9189                        return Ok(false);
9190                    }
9191                }
9192                Ok(true)
9193            }
9194        }
9195        let collector = Collector {
9196            out: Default::default(),
9197        };
9198        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9199        collector.out.into_inner()
9200    }
9201
9202    fn empty_exec_outcome_with_var_solutions(
9203        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9204    ) -> ExecOutcome {
9205        ExecOutcome {
9206            variables: Default::default(),
9207            operations: Default::default(),
9208            artifact_graph: Default::default(),
9209            scene_objects: Default::default(),
9210            source_range_to_object: Default::default(),
9211            var_solutions,
9212            refactor_metadata: Default::default(),
9213            issues: Default::default(),
9214            filenames: Default::default(),
9215            default_planes: Default::default(),
9216        }
9217    }
9218
9219    /// Happy path: commit a var solution to a `var N` inside a sketch block
9220    /// using a correct NodePath. Confirms the node-path code path produces the
9221    /// expected source mutation.
9222    #[test]
9223    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9224        let initial_source = "\
9225sketch(on = XY) {
9226  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9227}
9228";
9229        let program = Program::parse(initial_source).unwrap().0.unwrap();
9230        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9231        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9232        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9233
9234        let mut frontend = FrontendState::new();
9235        frontend.program = program;
9236
9237        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9238            literal_range,
9239            Some(node_path),
9240            Number {
9241                value: 25.0,
9242                units: NumericSuffix::Mm,
9243            },
9244        )]);
9245
9246        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9247
9248        insta::assert_snapshot!(
9249            "test_commit_var_solution_by_node_path_updates_sketch_var",
9250            source_delta.text
9251        );
9252    }
9253
9254    /// Whitespace inserted earlier in the source shifts the original SketchVar
9255    /// SourceRange. With NodePath propagation the commit should still target
9256    /// the right `var`. We simulate this by collecting node_paths against a
9257    /// "compact" source, then loading the frontend with a "padded" source
9258    /// (whose byte offsets differ), and feeding the original (now stale)
9259    /// source range plus the correct node_path back into the commit.
9260    #[test]
9261    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9262        let compact_source = "\
9263sketch(on = XY) {
9264  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9265}
9266";
9267        let padded_source = "\
9268// added comment\n// added comment\n\nsketch(on = XY) {
9269  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9270}
9271";
9272        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9273        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9274
9275        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9276            .into_iter()
9277            .next()
9278            .expect("expected `var 10mm` in compact source");
9279        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9280            .into_iter()
9281            .next()
9282            .expect("expected `var 10mm` in padded source");
9283
9284        assert_ne!(
9285            compact_match.0, padded_match.0,
9286            "byte offsets must differ for this test to be meaningful"
9287        );
9288        assert_eq!(
9289            compact_match.1, padded_match.1,
9290            "node paths must agree across whitespace; that's the whole point of NodePath",
9291        );
9292
9293        let mut frontend = FrontendState::new();
9294        frontend.program = padded_program;
9295
9296        // Stale source range from the compact source + correct node_path.
9297        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9298            compact_match.0,
9299            Some(compact_match.1),
9300            Number {
9301                value: 30.0,
9302                units: NumericSuffix::Mm,
9303            },
9304        )]);
9305
9306        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9307
9308        insta::assert_snapshot!(
9309            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9310            source_delta.text
9311        );
9312    }
9313
9314    /// When multiple `var` declarations exist and the stale source range
9315    /// happens to land on a *different* var, the node_path must take
9316    /// precedence and the right var gets updated.
9317    #[test]
9318    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9319        let initial_source = "\
9320sketch(on = XY) {
9321  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9322}
9323";
9324        let program = Program::parse(initial_source).unwrap().0.unwrap();
9325
9326        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9327            .into_iter()
9328            .next()
9329            .expect("expected `var 10mm`");
9330        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9331            .into_iter()
9332            .next()
9333            .expect("expected `var 20mm`");
9334
9335        let mut frontend = FrontendState::new();
9336        frontend.program = program;
9337
9338        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9339        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9340            var_20.0,
9341            Some(var_10.1),
9342            Number {
9343                value: 33.0,
9344                units: NumericSuffix::Mm,
9345            },
9346        )]);
9347
9348        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9349
9350        insta::assert_snapshot!(
9351            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9352            source_delta.text
9353        );
9354    }
9355
9356    /// Bare `var` (no initial literal) is only locatable via node_path. With
9357    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9358    /// solver solution should fill the initial value in. The
9359    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9360    /// is gated as an experimental feature; without it the re-parse of the
9361    /// recast source rejects bare `var` declarations.
9362    #[test]
9363    fn test_commit_var_solution_writes_back_into_bare_var() {
9364        let initial_source = "\
9365@settings(experimentalFeatures = allow, kclVersion = 2.0)
9366sketch(on = XY) {
9367  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9368}
9369";
9370        let program = Program::parse(initial_source).unwrap().0.unwrap();
9371
9372        // Pick the first bare `var`; collect_all_sketch_vars returns every
9373        // SketchVar, including bare ones.
9374        let bare = collect_all_sketch_vars(&program)
9375            .into_iter()
9376            .find(|(range, _)| {
9377                // The bare `var` is exactly the 3 characters "var".
9378                range.end() - range.start() == 3
9379            })
9380            .expect("expected at least one bare `var`");
9381
9382        let mut frontend = FrontendState::new();
9383        frontend.program = program;
9384
9385        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9386            bare.0,
9387            Some(bare.1),
9388            Number {
9389                value: 7.0,
9390                units: NumericSuffix::Mm,
9391            },
9392        )]);
9393
9394        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9395
9396        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9397        // written as an explicit suffix so the bare var commits with units.
9398        // The recast adds a blank line after the `@settings` annotation.
9399        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9400    }
9401
9402    #[tokio::test(flavor = "multi_thread")]
9403    async fn test_delete_point_without_var() {
9404        let initial_source = "\
9405sketch(on = XY) {
9406  point(at = [var 1, var 2])
9407  point(at = [var 3, var 4])
9408  point(at = [var 5, var 6])
9409}
9410";
9411
9412        let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414        let mut frontend = FrontendState::new();
9415
9416        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9417        let mock_ctx = ExecutorContext::new_mock(None).await;
9418        let version = Version(0);
9419
9420        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9421        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9422        let sketch_id = sketch_object.id;
9423        let sketch = expect_sketch(sketch_object);
9424
9425        let point_id = *sketch.segments.get(1).unwrap();
9426
9427        let (src_delta, scene_delta) = frontend
9428            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9429            .await
9430            .unwrap();
9431        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9432        assert_eq!(scene_delta.new_objects, vec![]);
9433        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9434
9435        ctx.close().await;
9436        mock_ctx.close().await;
9437    }
9438
9439    #[tokio::test(flavor = "multi_thread")]
9440    async fn test_delete_point_with_var() {
9441        let initial_source = "\
9442sketch(on = XY) {
9443  point(at = [var 1, var 2])
9444  point1 = point(at = [var 3, var 4])
9445  point(at = [var 5, var 6])
9446}
9447";
9448
9449        let program = Program::parse(initial_source).unwrap().0.unwrap();
9450
9451        let mut frontend = FrontendState::new();
9452
9453        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9454        let mock_ctx = ExecutorContext::new_mock(None).await;
9455        let version = Version(0);
9456
9457        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9458        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9459        let sketch_id = sketch_object.id;
9460        let sketch = expect_sketch(sketch_object);
9461
9462        let point_id = *sketch.segments.get(1).unwrap();
9463
9464        let (src_delta, scene_delta) = frontend
9465            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9466            .await
9467            .unwrap();
9468        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9469        assert_eq!(scene_delta.new_objects, vec![]);
9470        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9471
9472        ctx.close().await;
9473        mock_ctx.close().await;
9474    }
9475
9476    #[tokio::test(flavor = "multi_thread")]
9477    async fn test_delete_multiple_points() {
9478        let initial_source = "\
9479sketch(on = XY) {
9480  point(at = [var 1, var 2])
9481  point1 = point(at = [var 3, var 4])
9482  point(at = [var 5, var 6])
9483}
9484";
9485
9486        let program = Program::parse(initial_source).unwrap().0.unwrap();
9487
9488        let mut frontend = FrontendState::new();
9489
9490        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9491        let mock_ctx = ExecutorContext::new_mock(None).await;
9492        let version = Version(0);
9493
9494        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9495        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9496        let sketch_id = sketch_object.id;
9497
9498        let sketch = expect_sketch(sketch_object);
9499
9500        let point1_id = *sketch.segments.first().unwrap();
9501        let point2_id = *sketch.segments.get(1).unwrap();
9502
9503        let (src_delta, scene_delta) = frontend
9504            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9505            .await
9506            .unwrap();
9507        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9508        assert_eq!(scene_delta.new_objects, vec![]);
9509        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9510
9511        ctx.close().await;
9512        mock_ctx.close().await;
9513    }
9514
9515    #[tokio::test(flavor = "multi_thread")]
9516    async fn test_delete_coincident_constraint() {
9517        let initial_source = "\
9518sketch(on = XY) {
9519  point1 = point(at = [var 1, var 2])
9520  point2 = point(at = [var 3, var 4])
9521  coincident([point1, point2])
9522  point(at = [var 5, var 6])
9523}
9524";
9525
9526        let program = Program::parse(initial_source).unwrap().0.unwrap();
9527
9528        let mut frontend = FrontendState::new();
9529
9530        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9531        let mock_ctx = ExecutorContext::new_mock(None).await;
9532        let version = Version(0);
9533
9534        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9535        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9536        let sketch_id = sketch_object.id;
9537        let sketch = expect_sketch(sketch_object);
9538
9539        let coincident_id = *sketch.constraints.first().unwrap();
9540
9541        let (src_delta, scene_delta) = frontend
9542            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9543            .await
9544            .unwrap();
9545        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9546        assert_eq!(scene_delta.new_objects, vec![]);
9547        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9548
9549        ctx.close().await;
9550        mock_ctx.close().await;
9551    }
9552
9553    #[tokio::test(flavor = "multi_thread")]
9554    async fn test_delete_line_cascades_to_coincident_constraint() {
9555        let initial_source = "\
9556sketch(on = XY) {
9557  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9558  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9559  coincident([line1.end, line2.start])
9560}
9561";
9562
9563        let program = Program::parse(initial_source).unwrap().0.unwrap();
9564
9565        let mut frontend = FrontendState::new();
9566
9567        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9568        let mock_ctx = ExecutorContext::new_mock(None).await;
9569        let version = Version(0);
9570
9571        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9573        let sketch_id = sketch_object.id;
9574        let sketch = expect_sketch(sketch_object);
9575        let line_id = *sketch.segments.get(5).unwrap();
9576
9577        let (src_delta, scene_delta) = frontend
9578            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9579            .await
9580            .unwrap();
9581        insta::assert_snapshot!(
9582            "test_delete_line_cascades_to_coincident_constraint",
9583            src_delta.text.as_str()
9584        );
9585        assert_eq!(
9586            scene_delta.new_graph.objects.len(),
9587            5,
9588            "{:#?}",
9589            scene_delta.new_graph.objects
9590        );
9591
9592        ctx.close().await;
9593        mock_ctx.close().await;
9594    }
9595
9596    #[tokio::test(flavor = "multi_thread")]
9597    async fn test_delete_line_cascades_to_distance_constraint() {
9598        let initial_source = "\
9599sketch(on = XY) {
9600  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9601  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9602  distance([line1.end, line2.start]) == 10mm
9603}
9604";
9605
9606        let program = Program::parse(initial_source).unwrap().0.unwrap();
9607
9608        let mut frontend = FrontendState::new();
9609
9610        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9611        let mock_ctx = ExecutorContext::new_mock(None).await;
9612        let version = Version(0);
9613
9614        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9615        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9616        let sketch_id = sketch_object.id;
9617        let sketch = expect_sketch(sketch_object);
9618        let line_id = *sketch.segments.get(5).unwrap();
9619
9620        let (src_delta, scene_delta) = frontend
9621            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9622            .await
9623            .unwrap();
9624        insta::assert_snapshot!(
9625            "test_delete_line_cascades_to_distance_constraint",
9626            src_delta.text.as_str()
9627        );
9628        assert_eq!(
9629            scene_delta.new_graph.objects.len(),
9630            5,
9631            "{:#?}",
9632            scene_delta.new_graph.objects
9633        );
9634
9635        ctx.close().await;
9636        mock_ctx.close().await;
9637    }
9638
9639    #[tokio::test(flavor = "multi_thread")]
9640    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9641        let initial_source = "\
9642sketch(on = XY) {
9643  point1 = point(at = [var 1, var 2])
9644  point2 = point(at = [var 3, var 4])
9645  horizontalDistance([point1, point2]) == 10mm
9646}
9647";
9648
9649        let program = Program::parse(initial_source).unwrap().0.unwrap();
9650
9651        let mut frontend = FrontendState::new();
9652
9653        let mock_ctx = ExecutorContext::new_mock(None).await;
9654        let version = Version(0);
9655
9656        frontend.program = program.clone();
9657        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9658        frontend.update_state_after_exec(outcome, true);
9659        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9660        let sketch_id = sketch_object.id;
9661        let sketch = expect_sketch(sketch_object);
9662        let point2_id = *sketch.segments.get(1).unwrap();
9663
9664        let (src_delta, scene_delta) = frontend
9665            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9666            .await
9667            .unwrap();
9668        insta::assert_snapshot!(
9669            "test_delete_point_cascades_to_horizontal_distance_constraint",
9670            src_delta.text.as_str()
9671        );
9672        assert_eq!(
9673            scene_delta.new_graph.objects.len(),
9674            3,
9675            "{:#?}",
9676            scene_delta.new_graph.objects
9677        );
9678
9679        mock_ctx.close().await;
9680    }
9681
9682    #[tokio::test(flavor = "multi_thread")]
9683    async fn test_delete_line_cascades_to_fixed_constraint() {
9684        let initial_source = "\
9685sketch(on = XY) {
9686  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9687  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9688  fixed([line1.start, [0, 0]])
9689}
9690";
9691
9692        let program = Program::parse(initial_source).unwrap().0.unwrap();
9693
9694        let mut frontend = FrontendState::new();
9695
9696        let mock_ctx = ExecutorContext::new_mock(None).await;
9697        let version = Version(0);
9698
9699        frontend.program = program.clone();
9700        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9701        frontend.update_state_after_exec(outcome, true);
9702        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9703        let sketch_id = sketch_object.id;
9704        let sketch = expect_sketch(sketch_object);
9705        let line1_id = *sketch.segments.get(2).unwrap();
9706
9707        let (src_delta, scene_delta) = frontend
9708            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9709            .await
9710            .unwrap();
9711        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9712        assert_eq!(
9713            scene_delta.new_graph.objects.len(),
9714            5,
9715            "{:#?}",
9716            scene_delta.new_graph.objects
9717        );
9718
9719        mock_ctx.close().await;
9720    }
9721
9722    #[tokio::test(flavor = "multi_thread")]
9723    async fn test_delete_line_cascades_to_midpoint_constraint() {
9724        let initial_source = "\
9725sketch(on = XY) {
9726  point1 = point(at = [var 1, var 2])
9727  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9728  midpoint(line1, point = point1)
9729}
9730";
9731
9732        let program = Program::parse(initial_source).unwrap().0.unwrap();
9733
9734        let mut frontend = FrontendState::new();
9735
9736        let mock_ctx = ExecutorContext::new_mock(None).await;
9737        let version = Version(0);
9738
9739        frontend.program = program.clone();
9740        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9741        frontend.update_state_after_exec(outcome, true);
9742        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9743        let sketch_id = sketch_object.id;
9744        let sketch = expect_sketch(sketch_object);
9745        let line1_id = *sketch.segments.get(3).unwrap();
9746
9747        let (src_delta, scene_delta) = frontend
9748            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9749            .await
9750            .unwrap();
9751        insta::assert_snapshot!(
9752            "test_delete_line_cascades_to_midpoint_constraint",
9753            src_delta.text.as_str()
9754        );
9755        assert_eq!(
9756            scene_delta.new_graph.objects.len(),
9757            3,
9758            "{:#?}",
9759            scene_delta.new_graph.objects
9760        );
9761
9762        mock_ctx.close().await;
9763    }
9764
9765    #[tokio::test(flavor = "multi_thread")]
9766    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9767        let initial_source = "\
9768sketch(on = XY) {
9769  point1 = point(at = [var 1, var 2])
9770  point2 = point(at = [var 3, var 4])
9771  point3 = point(at = [var 5, var 6])
9772  coincident([point1, point2, point3])
9773}
9774";
9775
9776        let program = Program::parse(initial_source).unwrap().0.unwrap();
9777
9778        let mut frontend = FrontendState::new();
9779
9780        let mock_ctx = ExecutorContext::new_mock(None).await;
9781        let version = Version(0);
9782
9783        frontend.program = program.clone();
9784        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9785        frontend.update_state_after_exec(outcome, true);
9786        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9787        let sketch_id = sketch_object.id;
9788        let sketch = expect_sketch(sketch_object);
9789        let point3_id = *sketch.segments.get(2).unwrap();
9790
9791        let (src_delta, scene_delta) = frontend
9792            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9793            .await
9794            .unwrap();
9795        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9796        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9797        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9798        assert!(
9799            src_delta.text.contains("coincident([point1, point2])"),
9800            "{}",
9801            src_delta.text
9802        );
9803
9804        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9805        let sketch = expect_sketch(sketch_object);
9806        assert_eq!(sketch.segments.len(), 2);
9807        assert_eq!(sketch.constraints.len(), 1);
9808
9809        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9810        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9811            panic!("Expected constraint object");
9812        };
9813        let Constraint::Coincident(coincident) = constraint else {
9814            panic!("Expected coincident constraint");
9815        };
9816        assert_eq!(
9817            coincident.segments,
9818            sketch
9819                .segments
9820                .iter()
9821                .copied()
9822                .map(Into::into)
9823                .collect::<Vec<ConstraintSegment>>()
9824        );
9825
9826        mock_ctx.close().await;
9827    }
9828
9829    #[tokio::test(flavor = "multi_thread")]
9830    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9831        let initial_source = "\
9832sketch(on = XY) {
9833  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9834  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9835  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9836  equalLength([line1, line2, line3])
9837}
9838";
9839
9840        let program = Program::parse(initial_source).unwrap().0.unwrap();
9841
9842        let mut frontend = FrontendState::new();
9843
9844        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9845        let mock_ctx = ExecutorContext::new_mock(None).await;
9846        let version = Version(0);
9847
9848        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9849        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9850        let sketch_id = sketch_object.id;
9851        let sketch = expect_sketch(sketch_object);
9852        let line3_id = *sketch.segments.get(8).unwrap();
9853
9854        let (src_delta, scene_delta) = frontend
9855            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9856            .await
9857            .unwrap();
9858        insta::assert_snapshot!(
9859            "test_delete_line_preserves_multiline_equal_length_constraint",
9860            src_delta.text.as_str()
9861        );
9862
9863        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9864        let sketch = expect_sketch(sketch_object);
9865        assert_eq!(sketch.constraints.len(), 1);
9866
9867        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9868        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9869            panic!("Expected constraint object");
9870        };
9871        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9872            panic!("Expected lines equal length constraint");
9873        };
9874        assert_eq!(lines_equal_length.lines.len(), 2);
9875
9876        ctx.close().await;
9877        mock_ctx.close().await;
9878    }
9879
9880    #[tokio::test(flavor = "multi_thread")]
9881    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9882        let initial_source = "\
9883sketch(on = XY) {
9884  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9885  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9886  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9887  horizontal([line1.end, line2.start, line3.start])
9888}
9889";
9890
9891        let program = Program::parse(initial_source).unwrap().0.unwrap();
9892
9893        let mut frontend = FrontendState::new();
9894
9895        let mock_ctx = ExecutorContext::new_mock(None).await;
9896        let version = Version(0);
9897
9898        frontend.program = program.clone();
9899        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9900        frontend.update_state_after_exec(outcome, true);
9901        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9902        let sketch_id = sketch_object.id;
9903        let sketch = expect_sketch(sketch_object);
9904        let line1_id = *sketch.segments.get(2).unwrap();
9905
9906        let (src_delta, scene_delta) = frontend
9907            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9908            .await
9909            .unwrap();
9910        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9911        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9912        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9913        assert!(
9914            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9915            "{}",
9916            src_delta.text
9917        );
9918
9919        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9920        let sketch = expect_sketch(sketch_object);
9921        assert_eq!(sketch.constraints.len(), 1);
9922
9923        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9924        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9925            panic!("Expected constraint object");
9926        };
9927        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9928            panic!("Expected horizontal points constraint");
9929        };
9930        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9931        assert_eq!(*points, remaining_points);
9932
9933        mock_ctx.close().await;
9934    }
9935
9936    #[tokio::test(flavor = "multi_thread")]
9937    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9938        let initial_source = "\
9939sketch(on = XY) {
9940  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9941  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9942  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9943  vertical([line1.end, line2.start, line3.start])
9944}
9945";
9946
9947        let program = Program::parse(initial_source).unwrap().0.unwrap();
9948
9949        let mut frontend = FrontendState::new();
9950
9951        let mock_ctx = ExecutorContext::new_mock(None).await;
9952        let version = Version(0);
9953
9954        frontend.program = program.clone();
9955        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9956        frontend.update_state_after_exec(outcome, true);
9957        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9958        let sketch_id = sketch_object.id;
9959        let sketch = expect_sketch(sketch_object);
9960        let line1_id = *sketch.segments.get(2).unwrap();
9961
9962        let (src_delta, scene_delta) = frontend
9963            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9964            .await
9965            .unwrap();
9966        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9967        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9968        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9969        assert!(
9970            src_delta.text.contains("vertical([line2.start, line3.start])"),
9971            "{}",
9972            src_delta.text
9973        );
9974
9975        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9976        let sketch = expect_sketch(sketch_object);
9977        assert_eq!(sketch.constraints.len(), 1);
9978
9979        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9980        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9981            panic!("Expected constraint object");
9982        };
9983        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9984            panic!("Expected vertical points constraint");
9985        };
9986        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9987        assert_eq!(*points, remaining_points);
9988
9989        mock_ctx.close().await;
9990    }
9991
9992    #[tokio::test(flavor = "multi_thread")]
9993    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9994        let initial_source = "\
9995sketch(on = XY) {
9996  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9997  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9998  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9999  coincident([line1.end, line2.start, line3.start])
10000}
10001";
10002
10003        let program = Program::parse(initial_source).unwrap().0.unwrap();
10004
10005        let mut frontend = FrontendState::new();
10006
10007        let mock_ctx = ExecutorContext::new_mock(None).await;
10008        let version = Version(0);
10009
10010        frontend.program = program.clone();
10011        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10012        frontend.update_state_after_exec(outcome, true);
10013        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10014        let sketch_id = sketch_object.id;
10015        let sketch = expect_sketch(sketch_object);
10016        let line1_id = *sketch.segments.get(2).unwrap();
10017
10018        let (src_delta, scene_delta) = frontend
10019            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10020            .await
10021            .unwrap();
10022        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10023        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10024        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10025        assert!(
10026            src_delta.text.contains("coincident([line2.start, line3.start])"),
10027            "{}",
10028            src_delta.text
10029        );
10030
10031        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10032        let sketch = expect_sketch(sketch_object);
10033        assert_eq!(sketch.constraints.len(), 1);
10034
10035        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10036        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10037            panic!("Expected constraint object");
10038        };
10039        let Constraint::Coincident(coincident) = constraint else {
10040            panic!("Expected coincident constraint");
10041        };
10042        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10043        assert_eq!(coincident.segments, remaining_segments);
10044
10045        mock_ctx.close().await;
10046    }
10047
10048    #[tokio::test(flavor = "multi_thread")]
10049    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10050        let initial_source = "\
10051sketch(on = XY) {
10052  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10053  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10054  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10055  equalLength([line1, line2, line3])
10056}
10057";
10058
10059        let program = Program::parse(initial_source).unwrap().0.unwrap();
10060
10061        let mut frontend = FrontendState::new();
10062
10063        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10064        let mock_ctx = ExecutorContext::new_mock(None).await;
10065        let version = Version(0);
10066
10067        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10068        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10069        let sketch_id = sketch_object.id;
10070        let sketch = expect_sketch(sketch_object);
10071        let line2_id = *sketch.segments.get(5).unwrap();
10072        let line3_id = *sketch.segments.get(8).unwrap();
10073
10074        let (src_delta, scene_delta) = frontend
10075            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10076            .await
10077            .unwrap();
10078        insta::assert_snapshot!(
10079            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10080            src_delta.text.as_str()
10081        );
10082
10083        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10084        let sketch = expect_sketch(sketch_object);
10085        assert!(sketch.constraints.is_empty());
10086
10087        ctx.close().await;
10088        mock_ctx.close().await;
10089    }
10090
10091    #[tokio::test(flavor = "multi_thread")]
10092    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10093        let initial_source = "\
10094sketch(on = XY) {
10095  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10096  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10097  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10098  parallel([line1, line2, line3])
10099}
10100";
10101
10102        let program = Program::parse(initial_source).unwrap().0.unwrap();
10103
10104        let mut frontend = FrontendState::new();
10105
10106        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10107        let mock_ctx = ExecutorContext::new_mock(None).await;
10108        let version = Version(0);
10109
10110        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10111        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10112        let sketch_id = sketch_object.id;
10113        let sketch = expect_sketch(sketch_object);
10114        let line3_id = *sketch.segments.get(8).unwrap();
10115
10116        let (src_delta, scene_delta) = frontend
10117            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10118            .await
10119            .unwrap();
10120        insta::assert_snapshot!(
10121            "test_delete_line_preserves_multiline_parallel_constraint",
10122            src_delta.text.as_str()
10123        );
10124
10125        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10126        let sketch = expect_sketch(sketch_object);
10127        assert_eq!(sketch.constraints.len(), 1);
10128
10129        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10130        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10131            panic!("Expected constraint object");
10132        };
10133        let Constraint::Parallel(parallel) = constraint else {
10134            panic!("Expected parallel constraint");
10135        };
10136        assert_eq!(parallel.lines.len(), 2);
10137
10138        ctx.close().await;
10139        mock_ctx.close().await;
10140    }
10141
10142    #[tokio::test(flavor = "multi_thread")]
10143    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10144        let initial_source = "\
10145sketch(on = XY) {
10146  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10147  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10148  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10149  parallel([line1, line2, line3])
10150}
10151";
10152
10153        let program = Program::parse(initial_source).unwrap().0.unwrap();
10154
10155        let mut frontend = FrontendState::new();
10156
10157        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10158        let mock_ctx = ExecutorContext::new_mock(None).await;
10159        let version = Version(0);
10160
10161        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10162        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10163        let sketch_id = sketch_object.id;
10164        let sketch = expect_sketch(sketch_object);
10165        let line2_id = *sketch.segments.get(5).unwrap();
10166        let line3_id = *sketch.segments.get(8).unwrap();
10167
10168        let (src_delta, scene_delta) = frontend
10169            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10170            .await
10171            .unwrap();
10172        insta::assert_snapshot!(
10173            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10174            src_delta.text.as_str()
10175        );
10176
10177        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10178        let sketch = expect_sketch(sketch_object);
10179        assert!(sketch.constraints.is_empty());
10180
10181        ctx.close().await;
10182        mock_ctx.close().await;
10183    }
10184
10185    #[tokio::test(flavor = "multi_thread")]
10186    async fn test_delete_line_line_coincident_constraint() {
10187        let initial_source = "\
10188sketch(on = XY) {
10189  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10190  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10191  coincident([line1, line2])
10192}
10193";
10194
10195        let program = Program::parse(initial_source).unwrap().0.unwrap();
10196
10197        let mut frontend = FrontendState::new();
10198
10199        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10200        let mock_ctx = ExecutorContext::new_mock(None).await;
10201        let version = Version(0);
10202
10203        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10204        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10205        let sketch_id = sketch_object.id;
10206        let sketch = expect_sketch(sketch_object);
10207
10208        let coincident_id = *sketch.constraints.first().unwrap();
10209
10210        let (src_delta, scene_delta) = frontend
10211            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10212            .await
10213            .unwrap();
10214        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10215        assert_eq!(scene_delta.new_objects, vec![]);
10216        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10217
10218        ctx.close().await;
10219        mock_ctx.close().await;
10220    }
10221
10222    #[tokio::test(flavor = "multi_thread")]
10223    async fn test_two_points_coincident() {
10224        let initial_source = "\
10225sketch(on = XY) {
10226  point1 = point(at = [var 1, var 2])
10227  point(at = [3, 4])
10228}
10229";
10230
10231        let program = Program::parse(initial_source).unwrap().0.unwrap();
10232
10233        let mut frontend = FrontendState::new();
10234
10235        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10236        let mock_ctx = ExecutorContext::new_mock(None).await;
10237        let version = Version(0);
10238
10239        frontend.hack_set_program(&ctx, program).await.unwrap();
10240        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10241        let sketch_id = sketch_object.id;
10242        let sketch = expect_sketch(sketch_object);
10243        let point0_id = *sketch.segments.first().unwrap();
10244        let point1_id = *sketch.segments.get(1).unwrap();
10245
10246        let constraint = Constraint::Coincident(Coincident {
10247            segments: vec![point0_id.into(), point1_id.into()],
10248        });
10249        let (src_delta, scene_delta) = frontend
10250            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10251            .await
10252            .unwrap();
10253        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10254        assert_eq!(
10255            scene_delta.new_graph.objects.len(),
10256            5,
10257            "{:#?}",
10258            scene_delta.new_graph.objects
10259        );
10260
10261        ctx.close().await;
10262        mock_ctx.close().await;
10263    }
10264
10265    #[tokio::test(flavor = "multi_thread")]
10266    async fn test_three_points_coincident() {
10267        let initial_source = "\
10268sketch(on = XY) {
10269  point1 = point(at = [var 1, var 2])
10270  point(at = [var 3, var 4])
10271  point(at = [var 5, var 6])
10272}
10273";
10274
10275        let program = Program::parse(initial_source).unwrap().0.unwrap();
10276
10277        let mut frontend = FrontendState::new();
10278
10279        let mock_ctx = ExecutorContext::new_mock(None).await;
10280        let version = Version(0);
10281
10282        frontend.program = program.clone();
10283        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10284        frontend.update_state_after_exec(outcome, true);
10285        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10286        let sketch_id = sketch_object.id;
10287        let sketch = expect_sketch(sketch_object);
10288        let segments = sketch
10289            .segments
10290            .iter()
10291            .take(3)
10292            .copied()
10293            .map(Into::into)
10294            .collect::<Vec<ConstraintSegment>>();
10295
10296        let constraint = Constraint::Coincident(Coincident {
10297            segments: segments.clone(),
10298        });
10299        let (src_delta, scene_delta) = frontend
10300            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10301            .await
10302            .unwrap();
10303        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10304
10305        let constraint_object = scene_delta
10306            .new_graph
10307            .objects
10308            .iter()
10309            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10310            .unwrap();
10311
10312        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10313            panic!("expected a constraint object");
10314        };
10315
10316        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10317
10318        mock_ctx.close().await;
10319    }
10320
10321    #[tokio::test(flavor = "multi_thread")]
10322    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10323        let initial_source = "\
10324sketch(on = XY) {
10325  point1 = point(at = [var 1, var 2])
10326  point2 = point(at = [var 3, var 4])
10327  point3 = point(at = [var 5, var 6])
10328  coincident([point1, point2, point3])
10329}
10330";
10331
10332        let program = Program::parse(initial_source).unwrap().0.unwrap();
10333
10334        let mut frontend = FrontendState::new();
10335
10336        let ctx = ExecutorContext::new_mock(None).await;
10337        frontend.program = program.clone();
10338        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10339        frontend.update_state_after_exec(outcome, true);
10340
10341        let constraint_object = frontend
10342            .scene_graph
10343            .objects
10344            .iter()
10345            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10346            .unwrap();
10347        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10348            panic!("expected a constraint object");
10349        };
10350
10351        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10352        let sketch = expect_sketch(sketch_object);
10353        let expected_segments = sketch
10354            .segments
10355            .iter()
10356            .take(3)
10357            .copied()
10358            .map(Into::into)
10359            .collect::<Vec<ConstraintSegment>>();
10360
10361        assert_eq!(
10362            constraint,
10363            &Constraint::Coincident(Coincident {
10364                segments: expected_segments,
10365            })
10366        );
10367
10368        ctx.close().await;
10369    }
10370
10371    #[tokio::test(flavor = "multi_thread")]
10372    async fn test_point_origin_coincident_preserves_order() {
10373        let initial_source = "\
10374sketch(on = XY) {
10375  point(at = [var 1, var 2])
10376}
10377";
10378
10379        for (origin_first, snapshot_name) in [
10380            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10381            (false, "test_point_origin_coincident_preserves_order_point_first"),
10382        ] {
10383            let program = Program::parse(initial_source).unwrap().0.unwrap();
10384
10385            let mut frontend = FrontendState::new();
10386
10387            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10388            let mock_ctx = ExecutorContext::new_mock(None).await;
10389            let version = Version(0);
10390
10391            frontend.hack_set_program(&ctx, program).await.unwrap();
10392            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10393            let sketch_id = sketch_object.id;
10394            let sketch = expect_sketch(sketch_object);
10395            let point_id = *sketch.segments.first().unwrap();
10396
10397            let segments = if origin_first {
10398                vec![ConstraintSegment::ORIGIN, point_id.into()]
10399            } else {
10400                vec![point_id.into(), ConstraintSegment::ORIGIN]
10401            };
10402            let constraint = Constraint::Coincident(Coincident {
10403                segments: segments.clone(),
10404            });
10405            let (src_delta, scene_delta) = frontend
10406                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10407                .await
10408                .unwrap();
10409            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10410
10411            let constraint_object = scene_delta
10412                .new_graph
10413                .objects
10414                .iter()
10415                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10416                .unwrap();
10417
10418            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10419                panic!("expected a constraint object");
10420            };
10421
10422            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10423
10424            ctx.close().await;
10425            mock_ctx.close().await;
10426        }
10427    }
10428
10429    #[tokio::test(flavor = "multi_thread")]
10430    async fn test_coincident_of_line_end_points() {
10431        let initial_source = "\
10432sketch(on = XY) {
10433  line(start = [var 1, var 2], end = [var 3, var 4])
10434  line(start = [var 5, var 6], end = [var 7, var 8])
10435}
10436";
10437
10438        let program = Program::parse(initial_source).unwrap().0.unwrap();
10439
10440        let mut frontend = FrontendState::new();
10441
10442        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10443        let mock_ctx = ExecutorContext::new_mock(None).await;
10444        let version = Version(0);
10445
10446        frontend.hack_set_program(&ctx, program).await.unwrap();
10447        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10448        let sketch_id = sketch_object.id;
10449        let sketch = expect_sketch(sketch_object);
10450        let point0_id = *sketch.segments.get(1).unwrap();
10451        let point1_id = *sketch.segments.get(3).unwrap();
10452
10453        let constraint = Constraint::Coincident(Coincident {
10454            segments: vec![point0_id.into(), point1_id.into()],
10455        });
10456        let (src_delta, scene_delta) = frontend
10457            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10458            .await
10459            .unwrap();
10460        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10461        assert_eq!(
10462            scene_delta.new_graph.objects.len(),
10463            9,
10464            "{:#?}",
10465            scene_delta.new_graph.objects
10466        );
10467
10468        ctx.close().await;
10469        mock_ctx.close().await;
10470    }
10471
10472    #[tokio::test(flavor = "multi_thread")]
10473    async fn test_coincident_of_line_point_and_circle_segment() {
10474        let initial_source = "\
10475sketch(on = XY) {
10476  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10477  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10478}
10479";
10480        let program = Program::parse(initial_source).unwrap().0.unwrap();
10481        let mut frontend = FrontendState::new();
10482
10483        let mock_ctx = ExecutorContext::new_mock(None).await;
10484        let version = Version(0);
10485
10486        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10487        frontend.program = program;
10488        frontend.update_state_after_exec(outcome, true);
10489        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10490        let sketch_id = sketch_object.id;
10491        let sketch = expect_sketch(sketch_object);
10492
10493        let circle_id = sketch
10494            .segments
10495            .iter()
10496            .copied()
10497            .find(|seg_id| {
10498                matches!(
10499                    &frontend.scene_graph.objects[seg_id.0].kind,
10500                    ObjectKind::Segment {
10501                        segment: Segment::Circle(_)
10502                    }
10503                )
10504            })
10505            .expect("Expected a circle segment in sketch");
10506        let line_id = frontend
10507            .scene_graph
10508            .objects
10509            .iter()
10510            .find_map(|obj| match &obj.kind {
10511                ObjectKind::Segment {
10512                    segment: Segment::Line(line),
10513                } if line.owner.is_none() => Some(obj.id),
10514                _ => None,
10515            })
10516            .expect("Expected a standalone line segment in scene graph");
10517
10518        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10519            ObjectKind::Segment {
10520                segment: Segment::Line(line),
10521            } => line.start,
10522            _ => panic!("Expected line segment object"),
10523        };
10524
10525        let constraint = Constraint::Coincident(Coincident {
10526            segments: vec![line_start_point_id.into(), circle_id.into()],
10527        });
10528        let (src_delta, _scene_delta) = frontend
10529            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10530            .await
10531            .unwrap();
10532        insta::assert_snapshot!(
10533            "test_coincident_of_line_point_and_circle_segment",
10534            src_delta.text.as_str()
10535        );
10536
10537        mock_ctx.close().await;
10538    }
10539
10540    #[tokio::test(flavor = "multi_thread")]
10541    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10542        // Test that attempting an invalid coincident constraint (arc and line)
10543        // doesn't corrupt the state, allowing subsequent operations to work.
10544        // This test verifies the transactional fix in add_constraint that prevents
10545        // state corruption when invalid constraints are attempted.
10546        // Example: coincident constraint between an arc segment and a straight line segment
10547        // is geometrically invalid and should fail, but state should remain intact.
10548        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10549        let program = Program::empty();
10550
10551        let mut frontend = FrontendState::new();
10552        frontend.program = program;
10553
10554        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10555        let mock_ctx = ExecutorContext::new_mock(None).await;
10556        let version = Version(0);
10557
10558        let sketch_args = SketchCtor {
10559            on: Plane::Default(PlaneName::Xy),
10560        };
10561        let (_src_delta, _scene_delta, sketch_id) = frontend
10562            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10563            .await
10564            .unwrap();
10565
10566        // Add an arc segment
10567        let arc_ctor = ArcCtor {
10568            start: Point2d {
10569                x: Expr::Var(Number {
10570                    value: 0.0,
10571                    units: NumericSuffix::Mm,
10572                }),
10573                y: Expr::Var(Number {
10574                    value: 0.0,
10575                    units: NumericSuffix::Mm,
10576                }),
10577            },
10578            end: Point2d {
10579                x: Expr::Var(Number {
10580                    value: 10.0,
10581                    units: NumericSuffix::Mm,
10582                }),
10583                y: Expr::Var(Number {
10584                    value: 10.0,
10585                    units: NumericSuffix::Mm,
10586                }),
10587            },
10588            center: Point2d {
10589                x: Expr::Var(Number {
10590                    value: 10.0,
10591                    units: NumericSuffix::Mm,
10592                }),
10593                y: Expr::Var(Number {
10594                    value: 0.0,
10595                    units: NumericSuffix::Mm,
10596                }),
10597            },
10598            construction: None,
10599        };
10600        let (_src_delta, scene_delta) = frontend
10601            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10602            .await
10603            .unwrap();
10604        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10605        let arc_id = *scene_delta.new_objects.last().unwrap();
10606
10607        // Add a line segment
10608        let line_ctor = LineCtor {
10609            start: Point2d {
10610                x: Expr::Var(Number {
10611                    value: 20.0,
10612                    units: NumericSuffix::Mm,
10613                }),
10614                y: Expr::Var(Number {
10615                    value: 0.0,
10616                    units: NumericSuffix::Mm,
10617                }),
10618            },
10619            end: Point2d {
10620                x: Expr::Var(Number {
10621                    value: 30.0,
10622                    units: NumericSuffix::Mm,
10623                }),
10624                y: Expr::Var(Number {
10625                    value: 10.0,
10626                    units: NumericSuffix::Mm,
10627                }),
10628            },
10629            construction: None,
10630        };
10631        let (_src_delta, scene_delta) = frontend
10632            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10633            .await
10634            .unwrap();
10635        // The line is the last object in new_objects (after the 2 points: start, end)
10636        let line_id = *scene_delta.new_objects.last().unwrap();
10637
10638        // Attempt to add an invalid coincident constraint between arc and line
10639        // This should fail during execution, but state should remain intact
10640        let constraint = Constraint::Coincident(Coincident {
10641            segments: vec![arc_id.into(), line_id.into()],
10642        });
10643        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10644
10645        // The constraint addition should fail (invalid constraint)
10646        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10647
10648        // Verify state is not corrupted by checking that we can still access the scene graph
10649        // and that the original segments are still present with their source ranges
10650        let sketch_object_after =
10651            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10652        let sketch_after = expect_sketch(sketch_object_after);
10653
10654        // Verify both segments are still in the sketch
10655        assert!(
10656            sketch_after.segments.contains(&arc_id),
10657            "Arc segment should still exist after failed constraint"
10658        );
10659        assert!(
10660            sketch_after.segments.contains(&line_id),
10661            "Line segment should still exist after failed constraint"
10662        );
10663
10664        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10665        let arc_obj = frontend
10666            .scene_graph
10667            .objects
10668            .get(arc_id.0)
10669            .expect("Arc object should still be accessible");
10670        let line_obj = frontend
10671            .scene_graph
10672            .objects
10673            .get(line_id.0)
10674            .expect("Line object should still be accessible");
10675
10676        // Verify source ranges are still valid (not corrupted)
10677        // Just verify that the objects are still accessible and have the expected types
10678        match &arc_obj.kind {
10679            ObjectKind::Segment {
10680                segment: Segment::Arc(_),
10681            } => {}
10682            _ => panic!("Arc object should still be an arc segment"),
10683        }
10684        match &line_obj.kind {
10685            ObjectKind::Segment {
10686                segment: Segment::Line(_),
10687            } => {}
10688            _ => panic!("Line object should still be a line segment"),
10689        }
10690
10691        ctx.close().await;
10692        mock_ctx.close().await;
10693    }
10694
10695    #[tokio::test(flavor = "multi_thread")]
10696    async fn test_distance_two_points() {
10697        let initial_source = "\
10698sketch(on = XY) {
10699  point(at = [var 1, var 2])
10700  point(at = [var 3, var 4])
10701}
10702";
10703
10704        let program = Program::parse(initial_source).unwrap().0.unwrap();
10705
10706        let mut frontend = FrontendState::new();
10707
10708        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10709        let mock_ctx = ExecutorContext::new_mock(None).await;
10710        let version = Version(0);
10711
10712        frontend.hack_set_program(&ctx, program).await.unwrap();
10713        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10714        let sketch_id = sketch_object.id;
10715        let sketch = expect_sketch(sketch_object);
10716        let point0_id = *sketch.segments.first().unwrap();
10717        let point1_id = *sketch.segments.get(1).unwrap();
10718
10719        let constraint = Constraint::Distance(Distance {
10720            points: vec![point0_id.into(), point1_id.into()],
10721            distance: Number {
10722                value: 2.0,
10723                units: NumericSuffix::Mm,
10724            },
10725            label_position: None,
10726            source: Default::default(),
10727        });
10728        let (src_delta, scene_delta) = frontend
10729            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10730            .await
10731            .unwrap();
10732        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10733        assert_eq!(
10734            scene_delta.new_graph.objects.len(),
10735            5,
10736            "{:#?}",
10737            scene_delta.new_graph.objects
10738        );
10739
10740        ctx.close().await;
10741        mock_ctx.close().await;
10742    }
10743
10744    #[tokio::test(flavor = "multi_thread")]
10745    async fn test_distance_two_points_with_label() {
10746        let initial_source = "\
10747sketch(on = XY) {
10748  point(at = [var 1, var 2])
10749  point(at = [var 3, var 4])
10750}
10751";
10752
10753        let program = Program::parse(initial_source).unwrap().0.unwrap();
10754
10755        let mut frontend = FrontendState::new();
10756
10757        let mock_ctx = ExecutorContext::new_mock(None).await;
10758        let version = Version(0);
10759
10760        frontend.program = program.clone();
10761        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10762        frontend.update_state_after_exec(outcome, true);
10763        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10764        let sketch_id = sketch_object.id;
10765        let sketch = expect_sketch(sketch_object);
10766        let point0_id = *sketch.segments.first().unwrap();
10767        let point1_id = *sketch.segments.get(1).unwrap();
10768
10769        let label_position = Point2d {
10770            x: Number {
10771                value: 10.0,
10772                units: NumericSuffix::Mm,
10773            },
10774            y: Number {
10775                value: 11.0,
10776                units: NumericSuffix::Mm,
10777            },
10778        };
10779        let constraint = Constraint::Distance(Distance {
10780            points: vec![point0_id.into(), point1_id.into()],
10781            distance: Number {
10782                value: 2.0,
10783                units: NumericSuffix::Mm,
10784            },
10785            label_position: Some(label_position.clone()),
10786            source: Default::default(),
10787        });
10788        let (src_delta, scene_delta) = frontend
10789            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10790            .await
10791            .unwrap();
10792        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
10793
10794        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10795        let sketch = expect_sketch(sketch_object);
10796        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10797        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10798            panic!("Expected constraint object");
10799        };
10800        let Constraint::Distance(distance) = constraint else {
10801            panic!("Expected distance constraint");
10802        };
10803        assert_eq!(distance.label_position, Some(label_position));
10804
10805        mock_ctx.close().await;
10806    }
10807
10808    #[tokio::test(flavor = "multi_thread")]
10809    async fn test_edit_distance_constraint_label_position() {
10810        let initial_source = "\
10811sketch(on = XY) {
10812  point(at = [var 1, var 2])
10813  point(at = [var 3, var 2])
10814}
10815";
10816
10817        let program = Program::parse(initial_source).unwrap().0.unwrap();
10818
10819        let mut frontend = FrontendState::new();
10820
10821        let mock_ctx = ExecutorContext::new_mock(None).await;
10822        let version = Version(0);
10823
10824        frontend.program = program.clone();
10825        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10826        frontend.update_state_after_exec(outcome, true);
10827        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10828        let sketch_id = sketch_object.id;
10829        let sketch = expect_sketch(sketch_object);
10830        let point0_id = *sketch.segments.first().unwrap();
10831        let point1_id = *sketch.segments.get(1).unwrap();
10832
10833        let constraint = Constraint::Distance(Distance {
10834            points: vec![point0_id.into(), point1_id.into()],
10835            distance: Number {
10836                value: 2.0,
10837                units: NumericSuffix::Mm,
10838            },
10839            label_position: None,
10840            source: Default::default(),
10841        });
10842        let (_, scene_delta) = frontend
10843            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10844            .await
10845            .unwrap();
10846        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10847        let sketch = expect_sketch(sketch_object);
10848        let constraint_id = sketch.constraints[0];
10849        let label_position = Point2d {
10850            x: Number {
10851                value: 10.0,
10852                units: NumericSuffix::Mm,
10853            },
10854            y: Number {
10855                value: 11.0,
10856                units: NumericSuffix::Mm,
10857            },
10858        };
10859
10860        let (src_delta, scene_delta) = frontend
10861            .edit_distance_constraint_label_position(
10862                &mock_ctx,
10863                version,
10864                sketch_id,
10865                constraint_id,
10866                label_position.clone(),
10867                vec![],
10868            )
10869            .await
10870            .unwrap();
10871        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
10872
10873        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10874        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10875            panic!("Expected constraint object");
10876        };
10877        let Constraint::Distance(distance) = constraint else {
10878            panic!("Expected distance constraint");
10879        };
10880        assert_eq!(distance.label_position, Some(label_position));
10881
10882        mock_ctx.close().await;
10883    }
10884
10885    #[tokio::test(flavor = "multi_thread")]
10886    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
10887        let initial_source = "\
10888@settings(kclVersion = 2.0)
10889
10890sketch001 = sketch(on = XZ) {
10891  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
10892  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
10893  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
10894  vertical([line1.start, ORIGIN])
10895}
10896";
10897
10898        let program = Program::parse(initial_source).unwrap().0.unwrap();
10899        let mut frontend = FrontendState::new();
10900        let mock_ctx = ExecutorContext::new_mock(None).await;
10901        let version = Version(0);
10902
10903        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10904        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10905        let sketch_id = sketch_object.id;
10906        let sketch = expect_sketch(sketch_object);
10907        let constraint_id = sketch
10908            .constraints
10909            .iter()
10910            .copied()
10911            .find(|constraint_id| {
10912                matches!(
10913                    frontend.scene_graph.objects[constraint_id.0].kind,
10914                    ObjectKind::Constraint {
10915                        constraint: Constraint::Distance(_)
10916                    }
10917                )
10918            })
10919            .unwrap();
10920        let line1_id = sketch
10921            .segments
10922            .iter()
10923            .copied()
10924            .find(|segment_id| {
10925                matches!(
10926                    frontend.scene_graph.objects[segment_id.0].kind,
10927                    ObjectKind::Segment {
10928                        segment: Segment::Line(_)
10929                    }
10930                )
10931            })
10932            .unwrap();
10933        let label_position = Point2d {
10934            x: Number {
10935                value: 7.0,
10936                units: NumericSuffix::Mm,
10937            },
10938            y: Number {
10939                value: 9.0,
10940                units: NumericSuffix::Mm,
10941            },
10942        };
10943
10944        let (source_delta, scene_delta) = frontend
10945            .edit_segments_with_options(
10946                &mock_ctx,
10947                version,
10948                sketch_id,
10949                vec![ExistingSegmentCtor {
10950                    id: line1_id,
10951                    ctor: SegmentCtor::Line(LineCtor {
10952                        start: point_expr_mm(2.0, 15.55),
10953                        end: point_expr_mm(-4.03, 11.51),
10954                        construction: None,
10955                    }),
10956                }],
10957                EditSegmentsOptions {
10958                    anchor_segment_ids: Some(vec![]),
10959                    drag_anchors: vec![SegmentDragAnchor {
10960                        segment_id: line1_id,
10961                        target: label_position.clone(),
10962                    }],
10963                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
10964                        constraint_id,
10965                        label_position: label_position.clone(),
10966                    }],
10967                    commit_solved_initial_guesses: true,
10968                },
10969            )
10970            .await
10971            .unwrap();
10972
10973        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
10974        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
10975        let ObjectKind::Constraint {
10976            constraint: Constraint::Distance(distance),
10977        } = &constraint_object.kind
10978        else {
10979            panic!("Expected distance constraint object");
10980        };
10981        assert_eq!(distance.label_position, Some(label_position));
10982
10983        let snapped_label_position = Point2d {
10984            x: Number {
10985                value: 8.0,
10986                units: NumericSuffix::Mm,
10987            },
10988            y: Number {
10989                value: 10.0,
10990                units: NumericSuffix::Mm,
10991            },
10992        };
10993        let (source_delta, scene_delta) = frontend
10994            .edit_segments_with_options(
10995                &mock_ctx,
10996                version,
10997                sketch_id,
10998                vec![],
10999                EditSegmentsOptions {
11000                    anchor_segment_ids: Some(vec![line1_id]),
11001                    drag_anchors: vec![],
11002                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11003                        constraint_id,
11004                        label_position: snapped_label_position.clone(),
11005                    }],
11006                    commit_solved_initial_guesses: true,
11007                },
11008            )
11009            .await
11010            .unwrap();
11011
11012        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11013        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11014        let ObjectKind::Constraint {
11015            constraint: Constraint::Distance(distance),
11016        } = &constraint_object.kind
11017        else {
11018            panic!("Expected distance constraint object");
11019        };
11020        assert_eq!(distance.label_position, Some(snapped_label_position));
11021
11022        mock_ctx.close().await;
11023    }
11024
11025    #[tokio::test(flavor = "multi_thread")]
11026    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11027        let initial_source = "\
11028sketch(on = XY) {
11029  point1 = point(at = [var 0mm, var 0mm])
11030  point2 = point(at = [var 10mm, var 0mm])
11031  distance([point1, point2]) == 5mm
11032}
11033";
11034
11035        let program = Program::parse(initial_source).unwrap().0.unwrap();
11036        let mut frontend = FrontendState::new();
11037        let mock_ctx = ExecutorContext::new_mock(None).await;
11038        let version = Version(0);
11039
11040        frontend.program = program.clone();
11041        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11042        frontend.update_state_after_exec(outcome, true);
11043        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11044        let sketch_id = sketch_object.id;
11045        let sketch = expect_sketch(sketch_object);
11046        let point0_id = sketch.segments[0];
11047        let point1_id = sketch.segments[1];
11048        let constraint_id = sketch.constraints[0];
11049
11050        let edited_segments = vec![ExistingSegmentCtor {
11051            id: point0_id,
11052            ctor: SegmentCtor::Point(PointCtor {
11053                position: Point2d {
11054                    x: Expr::Var(Number {
11055                        value: 2.0,
11056                        units: NumericSuffix::Mm,
11057                    }),
11058                    y: Expr::Var(Number {
11059                        value: 1.0,
11060                        units: NumericSuffix::Mm,
11061                    }),
11062                },
11063            }),
11064        }];
11065        let (_, scene_delta) = frontend
11066            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11067            .await
11068            .unwrap();
11069        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11070        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11071
11072        let label_position = Point2d {
11073            x: Number {
11074                value: 3.0,
11075                units: NumericSuffix::Mm,
11076            },
11077            y: Number {
11078                value: 4.0,
11079                units: NumericSuffix::Mm,
11080            },
11081        };
11082        let (_, scene_delta) = frontend
11083            .edit_distance_constraint_label_position(
11084                &mock_ctx,
11085                version,
11086                sketch_id,
11087                constraint_id,
11088                label_position,
11089                vec![point0_id],
11090            )
11091            .await
11092            .unwrap();
11093
11094        assert_point_position_close(
11095            point_position(&scene_delta.new_graph, point0_id),
11096            point0_after_segment_edit,
11097        );
11098        assert_point_position_close(
11099            point_position(&scene_delta.new_graph, point1_id),
11100            point1_after_segment_edit,
11101        );
11102
11103        mock_ctx.close().await;
11104    }
11105
11106    #[tokio::test(flavor = "multi_thread")]
11107    async fn test_distance_point_line() {
11108        let initial_source = "\
11109sketch(on = XY) {
11110  point(at = [var 0, var 5])
11111  line(start = [var 0, var 0], end = [var 10, var 0])
11112}
11113";
11114
11115        let program = Program::parse(initial_source).unwrap().0.unwrap();
11116
11117        let mut frontend = FrontendState::new();
11118
11119        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11120        let mock_ctx = ExecutorContext::new_mock(None).await;
11121        let version = Version(0);
11122
11123        frontend.hack_set_program(&ctx, program).await.unwrap();
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 point_id = *sketch.segments.first().unwrap();
11128        let line_id = *sketch
11129            .segments
11130            .iter()
11131            .find(|segment_id| {
11132                matches!(
11133                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11134                    Some(ObjectKind::Segment {
11135                        segment: Segment::Line(_)
11136                    })
11137                )
11138            })
11139            .unwrap();
11140
11141        let label_position = Point2d {
11142            x: Number {
11143                value: 10.0,
11144                units: NumericSuffix::Mm,
11145            },
11146            y: Number {
11147                value: 11.0,
11148                units: NumericSuffix::Mm,
11149            },
11150        };
11151        let constraint = Constraint::Distance(Distance {
11152            points: vec![point_id.into(), line_id.into()],
11153            distance: Number {
11154                value: 5.0,
11155                units: NumericSuffix::Mm,
11156            },
11157            label_position: Some(label_position.clone()),
11158            source: Default::default(),
11159        });
11160        let (src_delta, scene_delta) = frontend
11161            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11162            .await
11163            .unwrap();
11164        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11165        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11166        let sketch = expect_sketch(sketch_object);
11167        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11168        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11169            panic!("Expected constraint object");
11170        };
11171        let Constraint::Distance(distance) = constraint else {
11172            panic!("Expected distance constraint");
11173        };
11174        assert_eq!(distance.label_position, Some(label_position));
11175
11176        ctx.close().await;
11177        mock_ctx.close().await;
11178    }
11179
11180    #[tokio::test(flavor = "multi_thread")]
11181    async fn test_distance_point_arc() {
11182        let initial_source = "\
11183sketch(on = XY) {
11184  point(at = [var 0, var 8])
11185  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11186}
11187";
11188
11189        let program = Program::parse(initial_source).unwrap().0.unwrap();
11190
11191        let mut frontend = FrontendState::new();
11192
11193        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11194        let mock_ctx = ExecutorContext::new_mock(None).await;
11195        let version = Version(0);
11196
11197        frontend.hack_set_program(&ctx, program).await.unwrap();
11198        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11199        let sketch_id = sketch_object.id;
11200        let sketch = expect_sketch(sketch_object);
11201        let point_id = *sketch.segments.first().unwrap();
11202        let arc_id = *sketch
11203            .segments
11204            .iter()
11205            .find(|segment_id| {
11206                matches!(
11207                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11208                    Some(ObjectKind::Segment {
11209                        segment: Segment::Arc(_)
11210                    })
11211                )
11212            })
11213            .unwrap();
11214
11215        let constraint = Constraint::Distance(Distance {
11216            points: vec![point_id.into(), arc_id.into()],
11217            distance: Number {
11218                value: 3.0,
11219                units: NumericSuffix::Mm,
11220            },
11221            label_position: None,
11222            source: Default::default(),
11223        });
11224        let (src_delta, _scene_delta) = frontend
11225            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11226            .await
11227            .unwrap();
11228        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11229
11230        ctx.close().await;
11231        mock_ctx.close().await;
11232    }
11233
11234    #[tokio::test(flavor = "multi_thread")]
11235    async fn test_distance_arc_origin() {
11236        let initial_source = "\
11237sketch001 = sketch(on = XY) {
11238  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11239}
11240";
11241
11242        let program = Program::parse(initial_source).unwrap().0.unwrap();
11243
11244        let mut frontend = FrontendState::new();
11245
11246        let mock_ctx = ExecutorContext::new_mock(None).await;
11247        let version = Version(0);
11248
11249        frontend.program = program.clone();
11250        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11251        frontend.update_state_after_exec(outcome, true);
11252        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11253        let sketch_id = sketch_object.id;
11254        let sketch = expect_sketch(sketch_object);
11255        let arc_id = *sketch
11256            .segments
11257            .iter()
11258            .find(|segment_id| {
11259                matches!(
11260                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11261                    Some(ObjectKind::Segment {
11262                        segment: Segment::Arc(_)
11263                    })
11264                )
11265            })
11266            .unwrap();
11267
11268        let constraint = Constraint::Distance(Distance {
11269            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11270            distance: Number {
11271                value: 3.0,
11272                units: NumericSuffix::Mm,
11273            },
11274            label_position: None,
11275            source: Default::default(),
11276        });
11277        let (src_delta, _scene_delta) = frontend
11278            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11279            .await
11280            .unwrap();
11281        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11282
11283        mock_ctx.close().await;
11284    }
11285
11286    #[tokio::test(flavor = "multi_thread")]
11287    async fn test_distance_line_origin() {
11288        let initial_source = "\
11289sketch(on = XY) {
11290  line(start = [var 5, var 0], end = [var 5, var 10])
11291}
11292";
11293
11294        let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296        let mut frontend = FrontendState::new();
11297
11298        let mock_ctx = ExecutorContext::new_mock(None).await;
11299        let version = Version(0);
11300
11301        frontend.program = program.clone();
11302        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11303        frontend.update_state_after_exec(outcome, true);
11304        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11305        let sketch_id = sketch_object.id;
11306        let sketch = expect_sketch(sketch_object);
11307        let line_id = *sketch
11308            .segments
11309            .iter()
11310            .find(|segment_id| {
11311                matches!(
11312                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11313                    Some(ObjectKind::Segment {
11314                        segment: Segment::Line(_)
11315                    })
11316                )
11317            })
11318            .unwrap();
11319
11320        let constraint = Constraint::Distance(Distance {
11321            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11322            distance: Number {
11323                value: 5.0,
11324                units: NumericSuffix::Mm,
11325            },
11326            label_position: None,
11327            source: Default::default(),
11328        });
11329        let (src_delta, _scene_delta) = frontend
11330            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11331            .await
11332            .unwrap();
11333        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11334
11335        mock_ctx.close().await;
11336    }
11337
11338    #[tokio::test(flavor = "multi_thread")]
11339    async fn test_distance_line_circle() {
11340        let initial_source = "\
11341sketch(on = XY) {
11342  line(start = [var -10, var 8], end = [var 10, var 8])
11343  circle(start = [var 5, var 0], center = [var 0, var 0])
11344}
11345";
11346
11347        let program = Program::parse(initial_source).unwrap().0.unwrap();
11348
11349        let mut frontend = FrontendState::new();
11350
11351        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11352        let mock_ctx = ExecutorContext::new_mock(None).await;
11353        let version = Version(0);
11354
11355        frontend.hack_set_program(&ctx, program).await.unwrap();
11356        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11357        let sketch_id = sketch_object.id;
11358        let sketch = expect_sketch(sketch_object);
11359        let line_id = *sketch
11360            .segments
11361            .iter()
11362            .find(|segment_id| {
11363                matches!(
11364                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11365                    Some(ObjectKind::Segment {
11366                        segment: Segment::Line(_)
11367                    })
11368                )
11369            })
11370            .unwrap();
11371        let circle_id = *sketch
11372            .segments
11373            .iter()
11374            .find(|segment_id| {
11375                matches!(
11376                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11377                    Some(ObjectKind::Segment {
11378                        segment: Segment::Circle(_)
11379                    })
11380                )
11381            })
11382            .unwrap();
11383
11384        let constraint = Constraint::Distance(Distance {
11385            points: vec![line_id.into(), circle_id.into()],
11386            distance: Number {
11387                value: 3.0,
11388                units: NumericSuffix::Mm,
11389            },
11390            label_position: None,
11391            source: Default::default(),
11392        });
11393        let (src_delta, _scene_delta) = frontend
11394            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11395            .await
11396            .unwrap();
11397        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11398
11399        ctx.close().await;
11400        mock_ctx.close().await;
11401    }
11402
11403    #[tokio::test(flavor = "multi_thread")]
11404    async fn test_distance_circle_arc() {
11405        let initial_source = "\
11406sketch(on = XY) {
11407  circle(start = [var 5, var 0], center = [var 0, var 0])
11408  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11409}
11410";
11411
11412        let program = Program::parse(initial_source).unwrap().0.unwrap();
11413
11414        let mut frontend = FrontendState::new();
11415
11416        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11417        let mock_ctx = ExecutorContext::new_mock(None).await;
11418        let version = Version(0);
11419
11420        frontend.hack_set_program(&ctx, program).await.unwrap();
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 circle_id = *sketch
11425            .segments
11426            .iter()
11427            .find(|segment_id| {
11428                matches!(
11429                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11430                    Some(ObjectKind::Segment {
11431                        segment: Segment::Circle(_)
11432                    })
11433                )
11434            })
11435            .unwrap();
11436        let arc_id = *sketch
11437            .segments
11438            .iter()
11439            .find(|segment_id| {
11440                matches!(
11441                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11442                    Some(ObjectKind::Segment {
11443                        segment: Segment::Arc(_)
11444                    })
11445                )
11446            })
11447            .unwrap();
11448
11449        let constraint = Constraint::Distance(Distance {
11450            points: vec![circle_id.into(), arc_id.into()],
11451            distance: Number {
11452                value: 3.0,
11453                units: NumericSuffix::Mm,
11454            },
11455            label_position: None,
11456            source: Default::default(),
11457        });
11458        let (src_delta, _scene_delta) = frontend
11459            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11460            .await
11461            .unwrap();
11462        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
11463
11464        ctx.close().await;
11465        mock_ctx.close().await;
11466    }
11467
11468    #[tokio::test(flavor = "multi_thread")]
11469    async fn test_distance_parallel_lines() {
11470        let initial_source = "\
11471sketch(on = XY) {
11472  line(start = [var 0, var 0], end = [var 10, var 0])
11473  line(start = [var 0, var 5], end = [var 10, var 5])
11474}
11475";
11476
11477        let program = Program::parse(initial_source).unwrap().0.unwrap();
11478
11479        let mut frontend = FrontendState::new();
11480
11481        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11482        let mock_ctx = ExecutorContext::new_mock(None).await;
11483        let version = Version(0);
11484
11485        frontend.hack_set_program(&ctx, program).await.unwrap();
11486        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11487        let sketch_id = sketch_object.id;
11488        let sketch = expect_sketch(sketch_object);
11489        let line_ids = sketch
11490            .segments
11491            .iter()
11492            .copied()
11493            .filter(|segment_id| {
11494                matches!(
11495                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11496                    Some(ObjectKind::Segment {
11497                        segment: Segment::Line(_)
11498                    })
11499                )
11500            })
11501            .collect::<Vec<_>>();
11502
11503        let constraint = Constraint::Distance(Distance {
11504            points: vec![line_ids[0].into(), line_ids[1].into()],
11505            distance: Number {
11506                value: 5.0,
11507                units: NumericSuffix::Mm,
11508            },
11509            label_position: None,
11510            source: Default::default(),
11511        });
11512        let (src_delta, _scene_delta) = frontend
11513            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11514            .await
11515            .unwrap();
11516        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
11517
11518        ctx.close().await;
11519        mock_ctx.close().await;
11520    }
11521
11522    #[tokio::test(flavor = "multi_thread")]
11523    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11524        let initial_source = "\
11525sketch(on = XY) {
11526  line(start = [var 0, var 0], end = [var 10, var 0])
11527  line(start = [var 0, var 0], end = [var 0, var 10])
11528}
11529";
11530
11531        let program = Program::parse(initial_source).unwrap().0.unwrap();
11532
11533        let mut frontend = FrontendState::new();
11534
11535        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11536        let mock_ctx = ExecutorContext::new_mock(None).await;
11537        let version = Version(0);
11538
11539        frontend.hack_set_program(&ctx, program).await.unwrap();
11540        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11541        let sketch_id = sketch_object.id;
11542        let sketch = expect_sketch(sketch_object);
11543        let line_ids = sketch
11544            .segments
11545            .iter()
11546            .copied()
11547            .filter(|segment_id| {
11548                matches!(
11549                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11550                    Some(ObjectKind::Segment {
11551                        segment: Segment::Line(_)
11552                    })
11553                )
11554            })
11555            .collect::<Vec<_>>();
11556
11557        let constraint = Constraint::Distance(Distance {
11558            points: vec![line_ids[0].into(), line_ids[1].into()],
11559            distance: Number {
11560                value: 5.0,
11561                units: NumericSuffix::Mm,
11562            },
11563            label_position: None,
11564            source: Default::default(),
11565        });
11566        let (src_delta, _scene_delta) = frontend
11567            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11568            .await
11569            .unwrap();
11570        insta::assert_snapshot!(
11571            "test_distance_non_parallel_lines_lowers_to_distance",
11572            src_delta.text.as_str()
11573        );
11574
11575        ctx.close().await;
11576        mock_ctx.close().await;
11577    }
11578
11579    #[tokio::test(flavor = "multi_thread")]
11580    async fn test_horizontal_distance_two_points() {
11581        let initial_source = "\
11582sketch(on = XY) {
11583  point(at = [var 1, var 2])
11584  point(at = [var 3, var 4])
11585}
11586";
11587
11588        let program = Program::parse(initial_source).unwrap().0.unwrap();
11589
11590        let mut frontend = FrontendState::new();
11591
11592        let mock_ctx = ExecutorContext::new_mock(None).await;
11593        let version = Version(0);
11594
11595        frontend.program = program.clone();
11596        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11597        frontend.update_state_after_exec(outcome, true);
11598        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11599        let sketch_id = sketch_object.id;
11600        let sketch = expect_sketch(sketch_object);
11601        let point0_id = *sketch.segments.first().unwrap();
11602        let point1_id = *sketch.segments.get(1).unwrap();
11603        let label_position = Point2d {
11604            x: Number {
11605                value: 10.0,
11606                units: NumericSuffix::Mm,
11607            },
11608            y: Number {
11609                value: 11.0,
11610                units: NumericSuffix::Mm,
11611            },
11612        };
11613
11614        let constraint = Constraint::HorizontalDistance(Distance {
11615            points: vec![point0_id.into(), point1_id.into()],
11616            distance: Number {
11617                value: 2.0,
11618                units: NumericSuffix::Mm,
11619            },
11620            label_position: Some(label_position.clone()),
11621            source: Default::default(),
11622        });
11623        let (src_delta, scene_delta) = frontend
11624            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11625            .await
11626            .unwrap();
11627        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
11628        assert_eq!(
11629            scene_delta.new_graph.objects.len(),
11630            5,
11631            "{:#?}",
11632            scene_delta.new_graph.objects
11633        );
11634        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11635        let sketch = expect_sketch(sketch_object);
11636        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11637        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11638            panic!("Expected constraint object");
11639        };
11640        let Constraint::HorizontalDistance(distance) = constraint else {
11641            panic!("Expected horizontal distance constraint");
11642        };
11643        assert_eq!(distance.label_position, Some(label_position));
11644
11645        mock_ctx.close().await;
11646    }
11647
11648    #[tokio::test(flavor = "multi_thread")]
11649    async fn test_radius_single_arc_segment() {
11650        let initial_source = "\
11651sketch(on = XY) {
11652  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11653}
11654";
11655
11656        let program = Program::parse(initial_source).unwrap().0.unwrap();
11657
11658        let mut frontend = FrontendState::new();
11659
11660        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11661        let mock_ctx = ExecutorContext::new_mock(None).await;
11662        let version = Version(0);
11663
11664        frontend.hack_set_program(&ctx, program).await.unwrap();
11665        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11666        let sketch_id = sketch_object.id;
11667        let sketch = expect_sketch(sketch_object);
11668        // Find the arc segment (not the points)
11669        let arc_id = sketch
11670            .segments
11671            .iter()
11672            .find(|&seg_id| {
11673                let obj = frontend.scene_graph.objects.get(seg_id.0);
11674                matches!(
11675                    obj.map(|o| &o.kind),
11676                    Some(ObjectKind::Segment {
11677                        segment: Segment::Arc(_)
11678                    })
11679                )
11680            })
11681            .unwrap();
11682
11683        let constraint = Constraint::Radius(Radius {
11684            arc: *arc_id,
11685            radius: Number {
11686                value: 5.0,
11687                units: NumericSuffix::Mm,
11688            },
11689            label_position: None,
11690            source: Default::default(),
11691        });
11692        let (src_delta, scene_delta) = frontend
11693            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11694            .await
11695            .unwrap();
11696        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
11697        assert_eq!(
11698            scene_delta.new_graph.objects.len(),
11699            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11700            "{:#?}",
11701            scene_delta.new_graph.objects
11702        );
11703
11704        ctx.close().await;
11705        mock_ctx.close().await;
11706    }
11707
11708    #[tokio::test(flavor = "multi_thread")]
11709    async fn test_radius_single_arc_segment_with_label_position() {
11710        let initial_source = "\
11711sketch(on = XY) {
11712  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11713}
11714";
11715
11716        let program = Program::parse(initial_source).unwrap().0.unwrap();
11717        let mut frontend = FrontendState::new();
11718        let mock_ctx = ExecutorContext::new_mock(None).await;
11719        let version = Version(0);
11720
11721        frontend.program = program.clone();
11722        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11723        frontend.update_state_after_exec(outcome, true);
11724        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11725        let sketch_id = sketch_object.id;
11726        let sketch = expect_sketch(sketch_object);
11727        let arc_id = sketch
11728            .segments
11729            .iter()
11730            .find(|&seg_id| {
11731                let obj = frontend.scene_graph.objects.get(seg_id.0);
11732                matches!(
11733                    obj.map(|o| &o.kind),
11734                    Some(ObjectKind::Segment {
11735                        segment: Segment::Arc(_)
11736                    })
11737                )
11738            })
11739            .unwrap();
11740
11741        let label_position = Point2d {
11742            x: Number {
11743                value: 10.0,
11744                units: NumericSuffix::Mm,
11745            },
11746            y: Number {
11747                value: 11.0,
11748                units: NumericSuffix::Mm,
11749            },
11750        };
11751        let constraint = Constraint::Radius(Radius {
11752            arc: *arc_id,
11753            radius: Number {
11754                value: 5.0,
11755                units: NumericSuffix::Mm,
11756            },
11757            label_position: Some(label_position.clone()),
11758            source: Default::default(),
11759        });
11760        let (src_delta, scene_delta) = frontend
11761            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11762            .await
11763            .unwrap();
11764        insta::assert_snapshot!(
11765            "test_radius_single_arc_segment_with_label_position",
11766            src_delta.text.as_str()
11767        );
11768
11769        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11770        let sketch = expect_sketch(sketch_object);
11771        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11772        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11773            panic!("Expected constraint object");
11774        };
11775        let Constraint::Radius(radius) = constraint else {
11776            panic!("Expected radius constraint");
11777        };
11778        assert_eq!(radius.label_position, Some(label_position));
11779
11780        mock_ctx.close().await;
11781    }
11782
11783    #[tokio::test(flavor = "multi_thread")]
11784    async fn test_edit_radius_constraint_label_position() {
11785        let initial_source = "\
11786sketch(on = XY) {
11787  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11788  radius(arc1) == 5mm
11789}
11790";
11791
11792        let program = Program::parse(initial_source).unwrap().0.unwrap();
11793        let mut frontend = FrontendState::new();
11794        let mock_ctx = ExecutorContext::new_mock(None).await;
11795        let version = Version(0);
11796
11797        frontend.program = program.clone();
11798        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11799        frontend.update_state_after_exec(outcome, true);
11800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11801        let sketch_id = sketch_object.id;
11802        let sketch = expect_sketch(sketch_object);
11803        let constraint_id = sketch.constraints[0];
11804        let label_position = Point2d {
11805            x: Number {
11806                value: 10.0,
11807                units: NumericSuffix::Mm,
11808            },
11809            y: Number {
11810                value: 11.0,
11811                units: NumericSuffix::Mm,
11812            },
11813        };
11814
11815        let (src_delta, scene_delta) = frontend
11816            .edit_distance_constraint_label_position(
11817                &mock_ctx,
11818                version,
11819                sketch_id,
11820                constraint_id,
11821                label_position.clone(),
11822                vec![],
11823            )
11824            .await
11825            .unwrap();
11826        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
11827
11828        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11829        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11830            panic!("Expected constraint object");
11831        };
11832        let Constraint::Radius(radius) = constraint else {
11833            panic!("Expected radius constraint");
11834        };
11835        assert_eq!(radius.label_position, Some(label_position));
11836
11837        mock_ctx.close().await;
11838    }
11839
11840    #[tokio::test(flavor = "multi_thread")]
11841    async fn test_vertical_distance_two_points() {
11842        let initial_source = "\
11843sketch(on = XY) {
11844  point(at = [var 1, var 2])
11845  point(at = [var 3, var 4])
11846}
11847";
11848
11849        let program = Program::parse(initial_source).unwrap().0.unwrap();
11850
11851        let mut frontend = FrontendState::new();
11852
11853        let mock_ctx = ExecutorContext::new_mock(None).await;
11854        let version = Version(0);
11855
11856        frontend.program = program.clone();
11857        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11858        frontend.update_state_after_exec(outcome, true);
11859        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11860        let sketch_id = sketch_object.id;
11861        let sketch = expect_sketch(sketch_object);
11862        let point0_id = *sketch.segments.first().unwrap();
11863        let point1_id = *sketch.segments.get(1).unwrap();
11864        let label_position = Point2d {
11865            x: Number {
11866                value: 10.0,
11867                units: NumericSuffix::Mm,
11868            },
11869            y: Number {
11870                value: 11.0,
11871                units: NumericSuffix::Mm,
11872            },
11873        };
11874
11875        let constraint = Constraint::VerticalDistance(Distance {
11876            points: vec![point0_id.into(), point1_id.into()],
11877            distance: Number {
11878                value: 2.0,
11879                units: NumericSuffix::Mm,
11880            },
11881            label_position: Some(label_position.clone()),
11882            source: Default::default(),
11883        });
11884        let (src_delta, scene_delta) = frontend
11885            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11886            .await
11887            .unwrap();
11888        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
11889        assert_eq!(
11890            scene_delta.new_graph.objects.len(),
11891            5,
11892            "{:#?}",
11893            scene_delta.new_graph.objects
11894        );
11895        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11896        let sketch = expect_sketch(sketch_object);
11897        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11898        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11899            panic!("Expected constraint object");
11900        };
11901        let Constraint::VerticalDistance(distance) = constraint else {
11902            panic!("Expected vertical distance constraint");
11903        };
11904        assert_eq!(distance.label_position, Some(label_position));
11905
11906        mock_ctx.close().await;
11907    }
11908
11909    #[tokio::test(flavor = "multi_thread")]
11910    async fn test_add_fixed_standalone_point() {
11911        let initial_source = "\
11912sketch(on = XY) {
11913  point(at = [var 1, var 2])
11914}
11915";
11916
11917        let program = Program::parse(initial_source).unwrap().0.unwrap();
11918
11919        let mut frontend = FrontendState::new();
11920
11921        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11922        let mock_ctx = ExecutorContext::new_mock(None).await;
11923        let version = Version(0);
11924
11925        frontend.hack_set_program(&ctx, program).await.unwrap();
11926        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11927        let sketch_id = sketch_object.id;
11928        let sketch = expect_sketch(sketch_object);
11929        let point_id = *sketch.segments.first().unwrap();
11930
11931        let (src_delta, scene_delta) = frontend
11932            .add_constraint(
11933                &mock_ctx,
11934                version,
11935                sketch_id,
11936                Constraint::Fixed(Fixed {
11937                    points: vec![FixedPoint {
11938                        point: point_id,
11939                        position: Point2d {
11940                            x: Number {
11941                                value: 2.0,
11942                                units: NumericSuffix::Mm,
11943                            },
11944                            y: Number {
11945                                value: 3.0,
11946                                units: NumericSuffix::Mm,
11947                            },
11948                        },
11949                    }],
11950                }),
11951            )
11952            .await
11953            .unwrap();
11954        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
11955        assert_eq!(
11956            scene_delta.new_graph.objects.len(),
11957            4,
11958            "{:#?}",
11959            scene_delta.new_graph.objects
11960        );
11961
11962        ctx.close().await;
11963        mock_ctx.close().await;
11964    }
11965
11966    #[tokio::test(flavor = "multi_thread")]
11967    async fn test_add_fixed_multiple_points() {
11968        let initial_source = "\
11969sketch(on = XY) {
11970  point(at = [var 1, var 2])
11971  point(at = [var 3, var 4])
11972}
11973";
11974
11975        let program = Program::parse(initial_source).unwrap().0.unwrap();
11976
11977        let mut frontend = FrontendState::new();
11978
11979        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11980        let mock_ctx = ExecutorContext::new_mock(None).await;
11981        let version = Version(0);
11982
11983        frontend.hack_set_program(&ctx, program).await.unwrap();
11984        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11985        let sketch_id = sketch_object.id;
11986        let sketch = expect_sketch(sketch_object);
11987        let point0_id = *sketch.segments.first().unwrap();
11988        let point1_id = *sketch.segments.get(1).unwrap();
11989
11990        let (src_delta, scene_delta) = frontend
11991            .add_constraint(
11992                &mock_ctx,
11993                version,
11994                sketch_id,
11995                Constraint::Fixed(Fixed {
11996                    points: vec![
11997                        FixedPoint {
11998                            point: point0_id,
11999                            position: Point2d {
12000                                x: Number {
12001                                    value: 2.0,
12002                                    units: NumericSuffix::Mm,
12003                                },
12004                                y: Number {
12005                                    value: 3.0,
12006                                    units: NumericSuffix::Mm,
12007                                },
12008                            },
12009                        },
12010                        FixedPoint {
12011                            point: point1_id,
12012                            position: Point2d {
12013                                x: Number {
12014                                    value: 4.0,
12015                                    units: NumericSuffix::Mm,
12016                                },
12017                                y: Number {
12018                                    value: 5.0,
12019                                    units: NumericSuffix::Mm,
12020                                },
12021                            },
12022                        },
12023                    ],
12024                }),
12025            )
12026            .await
12027            .unwrap();
12028        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12029        assert_eq!(
12030            scene_delta.new_graph.objects.len(),
12031            6,
12032            "{:#?}",
12033            scene_delta.new_graph.objects
12034        );
12035
12036        ctx.close().await;
12037        mock_ctx.close().await;
12038    }
12039
12040    #[tokio::test(flavor = "multi_thread")]
12041    async fn test_add_fixed_owned_point() {
12042        let initial_source = "\
12043sketch(on = XY) {
12044  line(start = [var 1, var 2], end = [var 3, var 4])
12045}
12046";
12047
12048        let program = Program::parse(initial_source).unwrap().0.unwrap();
12049
12050        let mut frontend = FrontendState::new();
12051
12052        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12053        let mock_ctx = ExecutorContext::new_mock(None).await;
12054        let version = Version(0);
12055
12056        frontend.hack_set_program(&ctx, program).await.unwrap();
12057        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12058        let sketch_id = sketch_object.id;
12059        let sketch = expect_sketch(sketch_object);
12060        let line_start_id = *sketch.segments.first().unwrap();
12061
12062        let (src_delta, scene_delta) = frontend
12063            .add_constraint(
12064                &mock_ctx,
12065                version,
12066                sketch_id,
12067                Constraint::Fixed(Fixed {
12068                    points: vec![FixedPoint {
12069                        point: line_start_id,
12070                        position: Point2d {
12071                            x: Number {
12072                                value: 2.0,
12073                                units: NumericSuffix::Mm,
12074                            },
12075                            y: Number {
12076                                value: 3.0,
12077                                units: NumericSuffix::Mm,
12078                            },
12079                        },
12080                    }],
12081                }),
12082            )
12083            .await
12084            .unwrap();
12085        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12086        assert_eq!(
12087            scene_delta.new_graph.objects.len(),
12088            6,
12089            "{:#?}",
12090            scene_delta.new_graph.objects
12091        );
12092
12093        ctx.close().await;
12094        mock_ctx.close().await;
12095    }
12096
12097    #[tokio::test(flavor = "multi_thread")]
12098    async fn test_radius_error_cases() {
12099        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12100        let mock_ctx = ExecutorContext::new_mock(None).await;
12101        let version = Version(0);
12102
12103        // Test: Single point should error
12104        let initial_source_point = "\
12105sketch(on = XY) {
12106  point(at = [var 1, var 2])
12107}
12108";
12109        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12110        let mut frontend_point = FrontendState::new();
12111        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12112        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12113        let sketch_id_point = sketch_object_point.id;
12114        let sketch_point = expect_sketch(sketch_object_point);
12115        let point_id = *sketch_point.segments.first().unwrap();
12116
12117        let constraint_point = Constraint::Radius(Radius {
12118            arc: point_id,
12119            radius: Number {
12120                value: 5.0,
12121                units: NumericSuffix::Mm,
12122            },
12123            label_position: None,
12124            source: Default::default(),
12125        });
12126        let result_point = frontend_point
12127            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12128            .await;
12129        assert!(result_point.is_err(), "Single point should error for radius");
12130
12131        // Test: Single line segment should error (only arc segments supported)
12132        let initial_source_line = "\
12133sketch(on = XY) {
12134  line(start = [var 1, var 2], end = [var 3, var 4])
12135}
12136";
12137        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12138        let mut frontend_line = FrontendState::new();
12139        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12140        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12141        let sketch_id_line = sketch_object_line.id;
12142        let sketch_line = expect_sketch(sketch_object_line);
12143        let line_id = *sketch_line.segments.first().unwrap();
12144
12145        let constraint_line = Constraint::Radius(Radius {
12146            arc: line_id,
12147            radius: Number {
12148                value: 5.0,
12149                units: NumericSuffix::Mm,
12150            },
12151            label_position: None,
12152            source: Default::default(),
12153        });
12154        let result_line = frontend_line
12155            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12156            .await;
12157        assert!(result_line.is_err(), "Single line segment should error for radius");
12158
12159        ctx.close().await;
12160        mock_ctx.close().await;
12161    }
12162
12163    #[tokio::test(flavor = "multi_thread")]
12164    async fn test_diameter_single_arc_segment() {
12165        let initial_source = "\
12166sketch(on = XY) {
12167  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12168}
12169";
12170
12171        let program = Program::parse(initial_source).unwrap().0.unwrap();
12172
12173        let mut frontend = FrontendState::new();
12174
12175        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12176        let mock_ctx = ExecutorContext::new_mock(None).await;
12177        let version = Version(0);
12178
12179        frontend.hack_set_program(&ctx, program).await.unwrap();
12180        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12181        let sketch_id = sketch_object.id;
12182        let sketch = expect_sketch(sketch_object);
12183        // Find the arc segment (not the points)
12184        let arc_id = sketch
12185            .segments
12186            .iter()
12187            .find(|&seg_id| {
12188                let obj = frontend.scene_graph.objects.get(seg_id.0);
12189                matches!(
12190                    obj.map(|o| &o.kind),
12191                    Some(ObjectKind::Segment {
12192                        segment: Segment::Arc(_)
12193                    })
12194                )
12195            })
12196            .unwrap();
12197
12198        let constraint = Constraint::Diameter(Diameter {
12199            arc: *arc_id,
12200            diameter: Number {
12201                value: 10.0,
12202                units: NumericSuffix::Mm,
12203            },
12204            label_position: None,
12205            source: Default::default(),
12206        });
12207        let (src_delta, scene_delta) = frontend
12208            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12209            .await
12210            .unwrap();
12211        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12212        assert_eq!(
12213            scene_delta.new_graph.objects.len(),
12214            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12215            "{:#?}",
12216            scene_delta.new_graph.objects
12217        );
12218
12219        ctx.close().await;
12220        mock_ctx.close().await;
12221    }
12222
12223    #[tokio::test(flavor = "multi_thread")]
12224    async fn test_diameter_single_arc_segment_with_label_position() {
12225        let initial_source = "\
12226sketch(on = XY) {
12227  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12228}
12229";
12230
12231        let program = Program::parse(initial_source).unwrap().0.unwrap();
12232        let mut frontend = FrontendState::new();
12233        let mock_ctx = ExecutorContext::new_mock(None).await;
12234        let version = Version(0);
12235
12236        frontend.program = program.clone();
12237        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12238        frontend.update_state_after_exec(outcome, true);
12239        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12240        let sketch_id = sketch_object.id;
12241        let sketch = expect_sketch(sketch_object);
12242        let arc_id = sketch
12243            .segments
12244            .iter()
12245            .find(|&seg_id| {
12246                let obj = frontend.scene_graph.objects.get(seg_id.0);
12247                matches!(
12248                    obj.map(|o| &o.kind),
12249                    Some(ObjectKind::Segment {
12250                        segment: Segment::Arc(_)
12251                    })
12252                )
12253            })
12254            .unwrap();
12255
12256        let label_position = Point2d {
12257            x: Number {
12258                value: 10.0,
12259                units: NumericSuffix::Mm,
12260            },
12261            y: Number {
12262                value: 11.0,
12263                units: NumericSuffix::Mm,
12264            },
12265        };
12266        let constraint = Constraint::Diameter(Diameter {
12267            arc: *arc_id,
12268            diameter: Number {
12269                value: 10.0,
12270                units: NumericSuffix::Mm,
12271            },
12272            label_position: Some(label_position.clone()),
12273            source: Default::default(),
12274        });
12275        let (src_delta, scene_delta) = frontend
12276            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12277            .await
12278            .unwrap();
12279        insta::assert_snapshot!(
12280            "test_diameter_single_arc_segment_with_label_position",
12281            src_delta.text.as_str()
12282        );
12283
12284        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12285        let sketch = expect_sketch(sketch_object);
12286        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12287        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12288            panic!("Expected constraint object");
12289        };
12290        let Constraint::Diameter(diameter) = constraint else {
12291            panic!("Expected diameter constraint");
12292        };
12293        assert_eq!(diameter.label_position, Some(label_position));
12294
12295        mock_ctx.close().await;
12296    }
12297
12298    #[tokio::test(flavor = "multi_thread")]
12299    async fn test_edit_diameter_constraint_label_position() {
12300        let initial_source = "\
12301sketch(on = XY) {
12302  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12303  diameter(arc1) == 10mm
12304}
12305";
12306
12307        let program = Program::parse(initial_source).unwrap().0.unwrap();
12308        let mut frontend = FrontendState::new();
12309        let mock_ctx = ExecutorContext::new_mock(None).await;
12310        let version = Version(0);
12311
12312        frontend.program = program.clone();
12313        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12314        frontend.update_state_after_exec(outcome, true);
12315        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12316        let sketch_id = sketch_object.id;
12317        let sketch = expect_sketch(sketch_object);
12318        let constraint_id = sketch.constraints[0];
12319        let label_position = Point2d {
12320            x: Number {
12321                value: 10.0,
12322                units: NumericSuffix::Mm,
12323            },
12324            y: Number {
12325                value: 11.0,
12326                units: NumericSuffix::Mm,
12327            },
12328        };
12329
12330        let (src_delta, scene_delta) = frontend
12331            .edit_distance_constraint_label_position(
12332                &mock_ctx,
12333                version,
12334                sketch_id,
12335                constraint_id,
12336                label_position.clone(),
12337                vec![],
12338            )
12339            .await
12340            .unwrap();
12341        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12342
12343        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12344        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12345            panic!("Expected constraint object");
12346        };
12347        let Constraint::Diameter(diameter) = constraint else {
12348            panic!("Expected diameter constraint");
12349        };
12350        assert_eq!(diameter.label_position, Some(label_position));
12351
12352        mock_ctx.close().await;
12353    }
12354
12355    #[tokio::test(flavor = "multi_thread")]
12356    async fn test_diameter_error_cases() {
12357        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12358        let mock_ctx = ExecutorContext::new_mock(None).await;
12359        let version = Version(0);
12360
12361        // Test: Single point should error
12362        let initial_source_point = "\
12363sketch(on = XY) {
12364  point(at = [var 1, var 2])
12365}
12366";
12367        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12368        let mut frontend_point = FrontendState::new();
12369        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12370        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12371        let sketch_id_point = sketch_object_point.id;
12372        let sketch_point = expect_sketch(sketch_object_point);
12373        let point_id = *sketch_point.segments.first().unwrap();
12374
12375        let constraint_point = Constraint::Diameter(Diameter {
12376            arc: point_id,
12377            diameter: Number {
12378                value: 10.0,
12379                units: NumericSuffix::Mm,
12380            },
12381            label_position: None,
12382            source: Default::default(),
12383        });
12384        let result_point = frontend_point
12385            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12386            .await;
12387        assert!(result_point.is_err(), "Single point should error for diameter");
12388
12389        // Test: Single line segment should error (only arc segments supported)
12390        let initial_source_line = "\
12391sketch(on = XY) {
12392  line(start = [var 1, var 2], end = [var 3, var 4])
12393}
12394";
12395        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12396        let mut frontend_line = FrontendState::new();
12397        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12398        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12399        let sketch_id_line = sketch_object_line.id;
12400        let sketch_line = expect_sketch(sketch_object_line);
12401        let line_id = *sketch_line.segments.first().unwrap();
12402
12403        let constraint_line = Constraint::Diameter(Diameter {
12404            arc: line_id,
12405            diameter: Number {
12406                value: 10.0,
12407                units: NumericSuffix::Mm,
12408            },
12409            label_position: None,
12410            source: Default::default(),
12411        });
12412        let result_line = frontend_line
12413            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12414            .await;
12415        assert!(result_line.is_err(), "Single line segment should error for diameter");
12416
12417        ctx.close().await;
12418        mock_ctx.close().await;
12419    }
12420
12421    #[tokio::test(flavor = "multi_thread")]
12422    async fn test_line_horizontal() {
12423        let initial_source = "\
12424sketch(on = XY) {
12425  line(start = [var 1, var 2], end = [var 3, var 4])
12426}
12427";
12428
12429        let program = Program::parse(initial_source).unwrap().0.unwrap();
12430
12431        let mut frontend = FrontendState::new();
12432
12433        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12434        let mock_ctx = ExecutorContext::new_mock(None).await;
12435        let version = Version(0);
12436
12437        frontend.hack_set_program(&ctx, program).await.unwrap();
12438        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12439        let sketch_id = sketch_object.id;
12440        let sketch = expect_sketch(sketch_object);
12441        let line1_id = *sketch.segments.get(2).unwrap();
12442
12443        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12444        let (src_delta, scene_delta) = frontend
12445            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12446            .await
12447            .unwrap();
12448        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
12449        assert_eq!(
12450            scene_delta.new_graph.objects.len(),
12451            6,
12452            "{:#?}",
12453            scene_delta.new_graph.objects
12454        );
12455
12456        ctx.close().await;
12457        mock_ctx.close().await;
12458    }
12459
12460    #[tokio::test(flavor = "multi_thread")]
12461    async fn test_control_point_spline_edge_horizontal() {
12462        let initial_source = "\
12463@settings(experimentalFeatures = allow)
12464splineSketch = sketch(on = XY) {
12465  controlPointSpline1 = controlPointSpline(points = [
12466    [var 0mm, var 0mm],
12467    [var 10mm, var 20mm],
12468    [var 20mm, var 0mm],
12469  ])
12470}
12471";
12472
12473        let program = Program::parse(initial_source).unwrap().0.unwrap();
12474
12475        let mut frontend = FrontendState::new();
12476
12477        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12478        let mock_ctx = ExecutorContext::new_mock(None).await;
12479        let version = Version(0);
12480
12481        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12482        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12483        let sketch_id = sketch_object.id;
12484        let sketch = expect_sketch(sketch_object);
12485        let spline_id = sketch
12486            .segments
12487            .iter()
12488            .copied()
12489            .find(|seg_id| {
12490                matches!(
12491                    &frontend.scene_graph.objects[seg_id.0].kind,
12492                    ObjectKind::Segment {
12493                        segment: Segment::ControlPointSpline(_)
12494                    }
12495                )
12496            })
12497            .expect("Expected a control point spline segment in sketch");
12498        let edge_id = frontend
12499            .scene_graph
12500            .objects
12501            .iter()
12502            .find_map(|obj| match &obj.kind {
12503                ObjectKind::Segment {
12504                    segment: Segment::Line(line),
12505                } if line.owner == Some(spline_id) => Some(obj.id),
12506                _ => None,
12507            })
12508            .expect("Expected an owned control-polygon edge");
12509
12510        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12511        let (src_delta, _) = frontend
12512            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12513            .await
12514            .unwrap();
12515        assert!(
12516            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12517            "Expected horizontal constraint on spline edge, got: {}",
12518            src_delta.text
12519        );
12520
12521        ctx.close().await;
12522        mock_ctx.close().await;
12523    }
12524
12525    #[tokio::test(flavor = "multi_thread")]
12526    async fn test_control_point_spline_edge_angle() {
12527        let initial_source = "\
12528@settings(experimentalFeatures = allow)
12529splineSketch = sketch(on = XY) {
12530  controlPointSpline1 = controlPointSpline(points = [
12531    [var 0mm, var 0mm],
12532    [var 10mm, var 20mm],
12533    [var 20mm, var 0mm],
12534  ])
12535
12536  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12537}
12538";
12539
12540        let program = Program::parse(initial_source).unwrap().0.unwrap();
12541
12542        let mut frontend = FrontendState::new();
12543
12544        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12545        let mock_ctx = ExecutorContext::new_mock(None).await;
12546        let version = Version(0);
12547
12548        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12549        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12550        let sketch_id = sketch_object.id;
12551        let sketch = expect_sketch(sketch_object);
12552        let spline_id = sketch
12553            .segments
12554            .iter()
12555            .copied()
12556            .find(|seg_id| {
12557                matches!(
12558                    &frontend.scene_graph.objects[seg_id.0].kind,
12559                    ObjectKind::Segment {
12560                        segment: Segment::ControlPointSpline(_)
12561                    }
12562                )
12563            })
12564            .expect("Expected a control point spline segment in sketch");
12565        let edge_id = frontend
12566            .scene_graph
12567            .objects
12568            .iter()
12569            .find_map(|obj| match &obj.kind {
12570                ObjectKind::Segment {
12571                    segment: Segment::Line(line),
12572                } if line.owner == Some(spline_id) => Some(obj.id),
12573                _ => None,
12574            })
12575            .expect("Expected an owned control-polygon edge");
12576        let line1_id = frontend
12577            .scene_graph
12578            .objects
12579            .iter()
12580            .find_map(|obj| match &obj.kind {
12581                ObjectKind::Segment {
12582                    segment: Segment::Line(line),
12583                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12584                _ => None,
12585            })
12586            .or_else(|| {
12587                sketch.segments.iter().copied().find(|seg_id| {
12588                    matches!(
12589                        &frontend.scene_graph.objects[seg_id.0].kind,
12590                        ObjectKind::Segment {
12591                            segment: Segment::Line(line),
12592                        } if line.owner.is_none()
12593                    )
12594                })
12595            })
12596            .expect("Expected a standalone line segment in sketch");
12597
12598        let constraint = Constraint::Angle(Angle {
12599            lines: vec![line1_id, edge_id],
12600            angle: Number {
12601                value: 30.0,
12602                units: NumericSuffix::Deg,
12603            },
12604            source: Default::default(),
12605        });
12606        let (src_delta, _) = frontend
12607            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12608            .await
12609            .unwrap();
12610        assert!(
12611            src_delta
12612                .text
12613                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12614            "Expected angle constraint on spline edge, got: {}",
12615            src_delta.text
12616        );
12617
12618        ctx.close().await;
12619        mock_ctx.close().await;
12620    }
12621
12622    #[tokio::test(flavor = "multi_thread")]
12623    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12624        let initial_source = "\
12625@settings(experimentalFeatures = allow)
12626splineSketch = sketch(on = XY) {
12627  spline1 = controlPointSpline(points = [
12628    [var 0mm, var 0mm],
12629    [var 10mm, var 20mm],
12630    [var 20mm, var 0mm],
12631  ])
12632  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12633  coincident([spline1.controls[1], spline1.edges[0]])
12634  coincident([spline1.controls[0], line1])
12635}
12636";
12637
12638        let program = Program::parse(initial_source).unwrap().0.unwrap();
12639
12640        let mut frontend = FrontendState::new();
12641
12642        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12643        let mock_ctx = ExecutorContext::new_mock(None).await;
12644
12645        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12646
12647        let ui_scene_graph = frontend.scene_graph_for_ui();
12648        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12649        let sketch = expect_sketch(sketch_object);
12650
12651        assert_eq!(
12652            sketch.constraints.len(),
12653            1,
12654            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12655        );
12656
12657        let visible_constraints = ui_scene_graph
12658            .objects
12659            .iter()
12660            .filter_map(|object| match &object.kind {
12661                ObjectKind::Constraint {
12662                    constraint: Constraint::Coincident(coincident),
12663                } => Some(coincident.clone()),
12664                _ => None,
12665            })
12666            .collect::<Vec<_>>();
12667
12668        assert_eq!(
12669            visible_constraints.len(),
12670            1,
12671            "Expected only one coincident constraint object in the UI scene graph"
12672        );
12673        assert_eq!(
12674            visible_constraints[0].get_segments().len(),
12675            2,
12676            "Expected the remaining visible coincident constraint to reference two segments"
12677        );
12678
12679        ctx.close().await;
12680        mock_ctx.close().await;
12681    }
12682
12683    #[tokio::test(flavor = "multi_thread")]
12684    async fn test_edit_control_point_spline_can_append_control_point() {
12685        let initial_source = "\
12686@settings(experimentalFeatures = allow)
12687splineSketch = sketch(on = XY) {
12688  controlPointSpline(points = [
12689    [var 0mm, var 0mm],
12690    [var 10mm, var 20mm],
12691    [var 20mm, var 0mm],
12692  ])
12693}
12694";
12695
12696        let program = Program::parse(initial_source).unwrap().0.unwrap();
12697
12698        let mut frontend = FrontendState::new();
12699
12700        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12701        let mock_ctx = ExecutorContext::new_mock(None).await;
12702        let version = Version(0);
12703
12704        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12705        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12706        let sketch_id = sketch_object.id;
12707        let sketch = expect_sketch(sketch_object);
12708        let spline_id = sketch
12709            .segments
12710            .iter()
12711            .copied()
12712            .find(|seg_id| {
12713                matches!(
12714                    &frontend.scene_graph.objects[seg_id.0].kind,
12715                    ObjectKind::Segment {
12716                        segment: Segment::ControlPointSpline(_)
12717                    }
12718                )
12719            })
12720            .expect("Expected a control point spline segment in sketch");
12721
12722        let ctor = ControlPointSplineCtor {
12723            points: vec![
12724                Point2d {
12725                    x: Expr::Var(Number {
12726                        value: 0.0,
12727                        units: NumericSuffix::Mm,
12728                    }),
12729                    y: Expr::Var(Number {
12730                        value: 0.0,
12731                        units: NumericSuffix::Mm,
12732                    }),
12733                },
12734                Point2d {
12735                    x: Expr::Var(Number {
12736                        value: 10.0,
12737                        units: NumericSuffix::Mm,
12738                    }),
12739                    y: Expr::Var(Number {
12740                        value: 20.0,
12741                        units: NumericSuffix::Mm,
12742                    }),
12743                },
12744                Point2d {
12745                    x: Expr::Var(Number {
12746                        value: 20.0,
12747                        units: NumericSuffix::Mm,
12748                    }),
12749                    y: Expr::Var(Number {
12750                        value: 0.0,
12751                        units: NumericSuffix::Mm,
12752                    }),
12753                },
12754                Point2d {
12755                    x: Expr::Var(Number {
12756                        value: 30.0,
12757                        units: NumericSuffix::Mm,
12758                    }),
12759                    y: Expr::Var(Number {
12760                        value: 10.0,
12761                        units: NumericSuffix::Mm,
12762                    }),
12763                },
12764            ],
12765            construction: None,
12766        };
12767
12768        let segments = vec![ExistingSegmentCtor {
12769            id: spline_id,
12770            ctor: SegmentCtor::ControlPointSpline(ctor),
12771        }];
12772        let (src_delta, scene_delta) = frontend
12773            .edit_segments(&mock_ctx, version, sketch_id, segments)
12774            .await
12775            .unwrap();
12776
12777        assert!(
12778            src_delta.text.contains("[var 30mm, var 10mm]"),
12779            "Expected appended spline control point in source, got: {}",
12780            src_delta.text
12781        );
12782
12783        assert!(
12784            scene_delta.invalidates_ids,
12785            "Expected appending a spline control point to invalidate ids"
12786        );
12787        let updated_spline = scene_delta
12788            .new_graph
12789            .objects
12790            .iter()
12791            .find_map(|obj| match &obj.kind {
12792                ObjectKind::Segment {
12793                    segment: Segment::ControlPointSpline(updated_spline),
12794                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12795                _ => None,
12796            })
12797            .expect("Expected edited scene graph to contain a four-point control point spline");
12798        assert_eq!(
12799            updated_spline.controls.len(),
12800            4,
12801            "Expected edited spline to expose four control points"
12802        );
12803
12804        ctx.close().await;
12805        mock_ctx.close().await;
12806    }
12807
12808    #[tokio::test(flavor = "multi_thread")]
12809    async fn test_line_vertical() {
12810        let initial_source = "\
12811sketch(on = XY) {
12812  line(start = [var 1, var 2], end = [var 3, var 4])
12813}
12814";
12815
12816        let program = Program::parse(initial_source).unwrap().0.unwrap();
12817
12818        let mut frontend = FrontendState::new();
12819
12820        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12821        let mock_ctx = ExecutorContext::new_mock(None).await;
12822        let version = Version(0);
12823
12824        frontend.hack_set_program(&ctx, program).await.unwrap();
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 line1_id = *sketch.segments.get(2).unwrap();
12829
12830        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12831        let (src_delta, scene_delta) = frontend
12832            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12833            .await
12834            .unwrap();
12835        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
12836        assert_eq!(
12837            scene_delta.new_graph.objects.len(),
12838            6,
12839            "{:#?}",
12840            scene_delta.new_graph.objects
12841        );
12842
12843        ctx.close().await;
12844        mock_ctx.close().await;
12845    }
12846
12847    #[tokio::test(flavor = "multi_thread")]
12848    async fn test_points_vertical() {
12849        let initial_source = "\
12850sketch001 = sketch(on = XY) {
12851  p0 = point(at = [var -2.23mm, var 3.1mm])
12852  pf = point(at = [4, 4])
12853}
12854";
12855
12856        let program = Program::parse(initial_source).unwrap().0.unwrap();
12857
12858        let mut frontend = FrontendState::new();
12859
12860        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12861        let mock_ctx = ExecutorContext::new_mock(None).await;
12862        let version = Version(0);
12863
12864        frontend.hack_set_program(&ctx, program).await.unwrap();
12865        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12866        let sketch_id = sketch_object.id;
12867        let sketch = expect_sketch(sketch_object);
12868        let point_ids = vec![
12869            sketch.segments.first().unwrap().to_owned(),
12870            sketch.segments.get(1).unwrap().to_owned(),
12871        ];
12872
12873        let constraint = Constraint::Vertical(Vertical::Points {
12874            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12875        });
12876        let (src_delta, scene_delta) = frontend
12877            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12878            .await
12879            .unwrap();
12880        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
12881        assert_eq!(
12882            scene_delta.new_graph.objects.len(),
12883            5,
12884            "{:#?}",
12885            scene_delta.new_graph.objects
12886        );
12887
12888        ctx.close().await;
12889        mock_ctx.close().await;
12890    }
12891
12892    #[tokio::test(flavor = "multi_thread")]
12893    async fn test_points_horizontal() {
12894        let initial_source = "\
12895sketch001 = sketch(on = XY) {
12896  p0 = point(at = [var -2.23mm, var 3.1mm])
12897  pf = point(at = [4, 4])
12898}
12899";
12900
12901        let program = Program::parse(initial_source).unwrap().0.unwrap();
12902
12903        let mut frontend = FrontendState::new();
12904
12905        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12906        let mock_ctx = ExecutorContext::new_mock(None).await;
12907        let version = Version(0);
12908
12909        frontend.hack_set_program(&ctx, program).await.unwrap();
12910        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12911        let sketch_id = sketch_object.id;
12912        let sketch = expect_sketch(sketch_object);
12913        let point_ids = vec![
12914            sketch.segments.first().unwrap().to_owned(),
12915            sketch.segments.get(1).unwrap().to_owned(),
12916        ];
12917
12918        let constraint = Constraint::Horizontal(Horizontal::Points {
12919            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12920        });
12921        let (src_delta, scene_delta) = frontend
12922            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12923            .await
12924            .unwrap();
12925        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
12926        assert_eq!(
12927            scene_delta.new_graph.objects.len(),
12928            5,
12929            "{:#?}",
12930            scene_delta.new_graph.objects
12931        );
12932
12933        ctx.close().await;
12934        mock_ctx.close().await;
12935    }
12936
12937    #[tokio::test(flavor = "multi_thread")]
12938    async fn test_point_horizontal_with_origin() {
12939        let initial_source = "\
12940sketch001 = sketch(on = XY) {
12941  p0 = point(at = [var -2.23mm, var 3.1mm])
12942}
12943";
12944
12945        let program = Program::parse(initial_source).unwrap().0.unwrap();
12946
12947        let mut frontend = FrontendState::new();
12948
12949        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12950        let mock_ctx = ExecutorContext::new_mock(None).await;
12951        let version = Version(0);
12952
12953        frontend.hack_set_program(&ctx, program).await.unwrap();
12954        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12955        let sketch_id = sketch_object.id;
12956        let sketch = expect_sketch(sketch_object);
12957        let point_id = *sketch.segments.first().unwrap();
12958
12959        let constraint = Constraint::Horizontal(Horizontal::Points {
12960            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12961        });
12962        let (src_delta, scene_delta) = frontend
12963            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12964            .await
12965            .unwrap();
12966        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
12967        assert_eq!(
12968            scene_delta.new_graph.objects.len(),
12969            4,
12970            "{:#?}",
12971            scene_delta.new_graph.objects
12972        );
12973
12974        ctx.close().await;
12975        mock_ctx.close().await;
12976    }
12977
12978    #[tokio::test(flavor = "multi_thread")]
12979    async fn test_lines_equal_length() {
12980        let initial_source = "\
12981sketch(on = XY) {
12982  line(start = [var 1, var 2], end = [var 3, var 4])
12983  line(start = [var 5, var 6], end = [var 7, var 8])
12984}
12985";
12986
12987        let program = Program::parse(initial_source).unwrap().0.unwrap();
12988
12989        let mut frontend = FrontendState::new();
12990
12991        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12992        let mock_ctx = ExecutorContext::new_mock(None).await;
12993        let version = Version(0);
12994
12995        frontend.hack_set_program(&ctx, program).await.unwrap();
12996        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12997        let sketch_id = sketch_object.id;
12998        let sketch = expect_sketch(sketch_object);
12999        let line1_id = *sketch.segments.get(2).unwrap();
13000        let line2_id = *sketch.segments.get(5).unwrap();
13001
13002        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13003            lines: vec![line1_id, line2_id],
13004        });
13005        let (src_delta, scene_delta) = frontend
13006            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13007            .await
13008            .unwrap();
13009        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13010        assert_eq!(
13011            scene_delta.new_graph.objects.len(),
13012            9,
13013            "{:#?}",
13014            scene_delta.new_graph.objects
13015        );
13016
13017        ctx.close().await;
13018        mock_ctx.close().await;
13019    }
13020
13021    #[tokio::test(flavor = "multi_thread")]
13022    async fn test_add_constraint_multi_line_equal_length() {
13023        let initial_source = "\
13024sketch(on = XY) {
13025  line(start = [var 1, var 2], end = [var 3, var 4])
13026  line(start = [var 5, var 6], end = [var 7, var 8])
13027  line(start = [var 9, var 10], end = [var 11, var 12])
13028}
13029";
13030
13031        let program = Program::parse(initial_source).unwrap().0.unwrap();
13032
13033        let mut frontend = FrontendState::new();
13034        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13035        let mock_ctx = ExecutorContext::new_mock(None).await;
13036        let version = Version(0);
13037
13038        frontend.hack_set_program(&ctx, program).await.unwrap();
13039        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13040        let sketch_id = sketch_object.id;
13041        let sketch = expect_sketch(sketch_object);
13042        let line1_id = *sketch.segments.get(2).unwrap();
13043        let line2_id = *sketch.segments.get(5).unwrap();
13044        let line3_id = *sketch.segments.get(8).unwrap();
13045
13046        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13047            lines: vec![line1_id, line2_id, line3_id],
13048        });
13049        let (src_delta, scene_delta) = frontend
13050            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13051            .await
13052            .unwrap();
13053        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13054        let constraints = scene_delta
13055            .new_graph
13056            .objects
13057            .iter()
13058            .filter_map(|obj| {
13059                let ObjectKind::Constraint { constraint } = &obj.kind else {
13060                    return None;
13061                };
13062                Some(constraint)
13063            })
13064            .collect::<Vec<_>>();
13065
13066        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13067        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13068            panic!("expected equal length constraint, got {:?}", constraints[0]);
13069        };
13070        assert_eq!(lines_equal_length.lines.len(), 3);
13071
13072        ctx.close().await;
13073        mock_ctx.close().await;
13074    }
13075
13076    #[tokio::test(flavor = "multi_thread")]
13077    async fn test_lines_parallel() {
13078        let initial_source = "\
13079sketch(on = XY) {
13080  line(start = [var 1, var 2], end = [var 3, var 4])
13081  line(start = [var 5, var 6], end = [var 7, var 8])
13082}
13083";
13084
13085        let program = Program::parse(initial_source).unwrap().0.unwrap();
13086
13087        let mut frontend = FrontendState::new();
13088
13089        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13090        let mock_ctx = ExecutorContext::new_mock(None).await;
13091        let version = Version(0);
13092
13093        frontend.hack_set_program(&ctx, program).await.unwrap();
13094        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13095        let sketch_id = sketch_object.id;
13096        let sketch = expect_sketch(sketch_object);
13097        let line1_id = *sketch.segments.get(2).unwrap();
13098        let line2_id = *sketch.segments.get(5).unwrap();
13099
13100        let constraint = Constraint::Parallel(Parallel {
13101            lines: vec![line1_id, line2_id],
13102        });
13103        let (src_delta, scene_delta) = frontend
13104            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13105            .await
13106            .unwrap();
13107        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13108        assert_eq!(
13109            scene_delta.new_graph.objects.len(),
13110            9,
13111            "{:#?}",
13112            scene_delta.new_graph.objects
13113        );
13114
13115        ctx.close().await;
13116        mock_ctx.close().await;
13117    }
13118
13119    #[tokio::test(flavor = "multi_thread")]
13120    async fn test_lines_parallel_multiline() {
13121        let initial_source = "\
13122sketch(on = XY) {
13123  line(start = [var 1, var 2], end = [var 3, var 4])
13124  line(start = [var 5, var 6], end = [var 7, var 8])
13125  line(start = [var 9, var 10], end = [var 11, var 12])
13126}
13127";
13128
13129        let program = Program::parse(initial_source).unwrap().0.unwrap();
13130
13131        let mut frontend = FrontendState::new();
13132
13133        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13134        let mock_ctx = ExecutorContext::new_mock(None).await;
13135        let version = Version(0);
13136
13137        frontend.hack_set_program(&ctx, program).await.unwrap();
13138        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13139        let sketch_id = sketch_object.id;
13140        let sketch = expect_sketch(sketch_object);
13141        let line1_id = *sketch.segments.get(2).unwrap();
13142        let line2_id = *sketch.segments.get(5).unwrap();
13143        let line3_id = *sketch.segments.get(8).unwrap();
13144
13145        let constraint = Constraint::Parallel(Parallel {
13146            lines: vec![line1_id, line2_id, line3_id],
13147        });
13148        let (src_delta, scene_delta) = frontend
13149            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13150            .await
13151            .unwrap();
13152        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13153
13154        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13155        let sketch = expect_sketch(sketch_object);
13156        assert_eq!(sketch.constraints.len(), 1);
13157
13158        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13159        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13160            panic!("Expected constraint object");
13161        };
13162        let Constraint::Parallel(parallel) = constraint else {
13163            panic!("Expected parallel constraint");
13164        };
13165        assert_eq!(parallel.lines.len(), 3);
13166
13167        ctx.close().await;
13168        mock_ctx.close().await;
13169    }
13170
13171    #[tokio::test(flavor = "multi_thread")]
13172    async fn test_lines_perpendicular() {
13173        let initial_source = "\
13174sketch(on = XY) {
13175  line(start = [var 1, var 2], end = [var 3, var 4])
13176  line(start = [var 5, var 6], end = [var 7, var 8])
13177}
13178";
13179
13180        let program = Program::parse(initial_source).unwrap().0.unwrap();
13181
13182        let mut frontend = FrontendState::new();
13183
13184        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13185        let mock_ctx = ExecutorContext::new_mock(None).await;
13186        let version = Version(0);
13187
13188        frontend.hack_set_program(&ctx, program).await.unwrap();
13189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13190        let sketch_id = sketch_object.id;
13191        let sketch = expect_sketch(sketch_object);
13192        let line1_id = *sketch.segments.get(2).unwrap();
13193        let line2_id = *sketch.segments.get(5).unwrap();
13194
13195        let constraint = Constraint::Perpendicular(Perpendicular {
13196            lines: vec![line1_id, line2_id],
13197        });
13198        let (src_delta, scene_delta) = frontend
13199            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13200            .await
13201            .unwrap();
13202        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13203        assert_eq!(
13204            scene_delta.new_graph.objects.len(),
13205            9,
13206            "{:#?}",
13207            scene_delta.new_graph.objects
13208        );
13209
13210        ctx.close().await;
13211        mock_ctx.close().await;
13212    }
13213
13214    #[tokio::test(flavor = "multi_thread")]
13215    async fn test_lines_angle() {
13216        let initial_source = "\
13217sketch(on = XY) {
13218  line(start = [var 1, var 2], end = [var 3, var 4])
13219  line(start = [var 5, var 6], end = [var 7, var 8])
13220}
13221";
13222
13223        let program = Program::parse(initial_source).unwrap().0.unwrap();
13224
13225        let mut frontend = FrontendState::new();
13226
13227        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13228        let mock_ctx = ExecutorContext::new_mock(None).await;
13229        let version = Version(0);
13230
13231        frontend.hack_set_program(&ctx, program).await.unwrap();
13232        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13233        let sketch_id = sketch_object.id;
13234        let sketch = expect_sketch(sketch_object);
13235        let line1_id = *sketch.segments.get(2).unwrap();
13236        let line2_id = *sketch.segments.get(5).unwrap();
13237
13238        let constraint = Constraint::Angle(Angle {
13239            lines: vec![line1_id, line2_id],
13240            angle: Number {
13241                value: 30.0,
13242                units: NumericSuffix::Deg,
13243            },
13244            source: Default::default(),
13245        });
13246        let (src_delta, scene_delta) = frontend
13247            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13248            .await
13249            .unwrap();
13250        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13251        assert_eq!(
13252            scene_delta.new_graph.objects.len(),
13253            9,
13254            "{:#?}",
13255            scene_delta.new_graph.objects
13256        );
13257
13258        ctx.close().await;
13259        mock_ctx.close().await;
13260    }
13261
13262    #[tokio::test(flavor = "multi_thread")]
13263    async fn test_segments_tangent() {
13264        let initial_source = "\
13265sketch(on = XY) {
13266  line(start = [var 1, var 2], end = [var 3, var 4])
13267  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13268}
13269";
13270
13271        let program = Program::parse(initial_source).unwrap().0.unwrap();
13272
13273        let mut frontend = FrontendState::new();
13274
13275        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13276        let mock_ctx = ExecutorContext::new_mock(None).await;
13277        let version = Version(0);
13278
13279        frontend.hack_set_program(&ctx, program).await.unwrap();
13280        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13281        let sketch_id = sketch_object.id;
13282        let sketch = expect_sketch(sketch_object);
13283        let line1_id = *sketch.segments.get(2).unwrap();
13284        let arc1_id = *sketch.segments.get(6).unwrap();
13285
13286        let constraint = Constraint::Tangent(Tangent {
13287            input: vec![line1_id, arc1_id],
13288        });
13289        let (src_delta, scene_delta) = frontend
13290            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13291            .await
13292            .unwrap();
13293        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13294        assert_eq!(
13295            scene_delta.new_graph.objects.len(),
13296            10,
13297            "{:#?}",
13298            scene_delta.new_graph.objects
13299        );
13300
13301        ctx.close().await;
13302        mock_ctx.close().await;
13303    }
13304
13305    #[tokio::test(flavor = "multi_thread")]
13306    async fn test_point_midpoint() {
13307        let initial_source = "\
13308sketch(on = XY) {
13309  point(at = [var 1, var 1])
13310  line(start = [var 0, var 0], end = [var 6, var 4])
13311}
13312";
13313
13314        let program = Program::parse(initial_source).unwrap().0.unwrap();
13315
13316        let mut frontend = FrontendState::new();
13317
13318        let ctx = ExecutorContext::new_mock(None).await;
13319        let version = Version(0);
13320
13321        frontend.program = program.clone();
13322        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13323        frontend.update_state_after_exec(outcome, true);
13324        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13325        let sketch_id = sketch_object.id;
13326        let sketch = expect_sketch(sketch_object);
13327        let point_id = *sketch.segments.first().unwrap();
13328        let line_id = *sketch.segments.get(3).unwrap();
13329
13330        let constraint = Constraint::Midpoint(Midpoint {
13331            point: ConstraintSegment::from(point_id),
13332            segment: line_id,
13333        });
13334        let (src_delta, scene_delta) = frontend
13335            .add_constraint(&ctx, version, sketch_id, constraint)
13336            .await
13337            .unwrap();
13338        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13339        assert_eq!(
13340            scene_delta.new_graph.objects.len(),
13341            7,
13342            "{:#?}",
13343            scene_delta.new_graph.objects
13344        );
13345
13346        ctx.close().await;
13347    }
13348
13349    #[tokio::test(flavor = "multi_thread")]
13350    async fn test_segments_symmetric() {
13351        let initial_source = "\
13352sketch(on = XY) {
13353  line(start = [var 0, var 0], end = [var 0, var 4])
13354  line(start = [var 4, var 0], end = [var 4, var 4])
13355  line(start = [var 2, var -1], end = [var 2, var 5])
13356}
13357";
13358
13359        let program = Program::parse(initial_source).unwrap().0.unwrap();
13360
13361        let mut frontend = FrontendState::new();
13362
13363        let ctx = ExecutorContext::new_mock(None).await;
13364        let version = Version(0);
13365
13366        frontend.program = program.clone();
13367        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13368        frontend.update_state_after_exec(outcome, true);
13369        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13370        let sketch_id = sketch_object.id;
13371        let sketch = expect_sketch(sketch_object);
13372        let line1_id = *sketch.segments.get(2).unwrap();
13373        let line2_id = *sketch.segments.get(5).unwrap();
13374        let axis_id = *sketch.segments.get(8).unwrap();
13375
13376        let constraint = Constraint::Symmetric(Symmetric {
13377            input: vec![line1_id, line2_id],
13378            axis: axis_id,
13379        });
13380        let (src_delta, scene_delta) = frontend
13381            .add_constraint(&ctx, version, sketch_id, constraint)
13382            .await
13383            .unwrap();
13384        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
13385        assert_eq!(
13386            scene_delta.new_graph.objects.len(),
13387            12,
13388            "{:#?}",
13389            scene_delta.new_graph.objects
13390        );
13391
13392        ctx.close().await;
13393    }
13394
13395    #[tokio::test(flavor = "multi_thread")]
13396    async fn test_point_arc_midpoint() {
13397        let initial_source = "\
13398sketch(on = XY) {
13399  point(at = [var 6, var 3])
13400  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13401}
13402";
13403
13404        let program = Program::parse(initial_source).unwrap().0.unwrap();
13405
13406        let mut frontend = FrontendState::new();
13407
13408        let ctx = ExecutorContext::new_mock(None).await;
13409        let version = Version(0);
13410
13411        frontend.program = program.clone();
13412        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13413        frontend.update_state_after_exec(outcome, true);
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 point_id = *sketch.segments.first().unwrap();
13418        let arc_id = *sketch.segments.get(4).unwrap();
13419
13420        let constraint = Constraint::Midpoint(Midpoint {
13421            point: ConstraintSegment::from(point_id),
13422            segment: arc_id,
13423        });
13424        let (src_delta, scene_delta) = frontend
13425            .add_constraint(&ctx, version, sketch_id, constraint)
13426            .await
13427            .unwrap();
13428        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
13429        assert_eq!(
13430            scene_delta.new_graph.objects.len(),
13431            8,
13432            "{:#?}",
13433            scene_delta.new_graph.objects
13434        );
13435
13436        ctx.close().await;
13437    }
13438
13439    #[tokio::test(flavor = "multi_thread")]
13440    async fn test_origin_line_midpoint() {
13441        let initial_source = "\
13442sketch(on = XY) {
13443  line(start = [var 0, var 0], end = [var 6, var 4])
13444}
13445";
13446
13447        let program = Program::parse(initial_source).unwrap().0.unwrap();
13448
13449        let mut frontend = FrontendState::new();
13450
13451        let ctx = ExecutorContext::new_mock(None).await;
13452        let version = Version(0);
13453
13454        frontend.program = program.clone();
13455        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13456        frontend.update_state_after_exec(outcome, true);
13457        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13458        let sketch_id = sketch_object.id;
13459        let sketch = expect_sketch(sketch_object);
13460        let line_id = *sketch.segments.get(2).unwrap();
13461
13462        let constraint = Constraint::Midpoint(Midpoint {
13463            point: ConstraintSegment::ORIGIN,
13464            segment: line_id,
13465        });
13466        let (src_delta, scene_delta) = frontend
13467            .add_constraint(&ctx, version, sketch_id, constraint)
13468            .await
13469            .unwrap();
13470        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
13471        assert_eq!(
13472            scene_delta.new_graph.objects.len(),
13473            6,
13474            "{:#?}",
13475            scene_delta.new_graph.objects
13476        );
13477
13478        ctx.close().await;
13479    }
13480
13481    #[tokio::test(flavor = "multi_thread")]
13482    async fn test_origin_arc_midpoint() {
13483        let initial_source = "\
13484sketch(on = XY) {
13485  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13486}
13487";
13488
13489        let program = Program::parse(initial_source).unwrap().0.unwrap();
13490
13491        let mut frontend = FrontendState::new();
13492
13493        let ctx = ExecutorContext::new_mock(None).await;
13494        let version = Version(0);
13495
13496        frontend.program = program.clone();
13497        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13498        frontend.update_state_after_exec(outcome, true);
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 arc_id = *sketch.segments.get(3).unwrap();
13503
13504        let constraint = Constraint::Midpoint(Midpoint {
13505            point: ConstraintSegment::ORIGIN,
13506            segment: arc_id,
13507        });
13508        let (src_delta, scene_delta) = frontend
13509            .add_constraint(&ctx, version, sketch_id, constraint)
13510            .await
13511            .unwrap();
13512        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
13513        assert_eq!(
13514            scene_delta.new_graph.objects.len(),
13515            7,
13516            "{:#?}",
13517            scene_delta.new_graph.objects
13518        );
13519
13520        ctx.close().await;
13521    }
13522
13523    #[tokio::test(flavor = "multi_thread")]
13524    async fn test_segments_symmetric_arcs() {
13525        let initial_source = "\
13526sketch(on = XY) {
13527  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13528  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13529  line(start = [var 0, var -10], end = [var 0, var 10])
13530}
13531";
13532
13533        let program = Program::parse(initial_source).unwrap().0.unwrap();
13534
13535        let mut frontend = FrontendState::new();
13536
13537        let ctx = ExecutorContext::new_mock(None).await;
13538        let version = Version(0);
13539
13540        frontend.program = program.clone();
13541        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13542        frontend.update_state_after_exec(outcome, true);
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 arc1_id = *sketch.segments.get(3).unwrap();
13547        let arc2_id = *sketch.segments.get(7).unwrap();
13548        let axis_id = *sketch.segments.get(10).unwrap();
13549
13550        let constraint = Constraint::Symmetric(Symmetric {
13551            input: vec![arc1_id, arc2_id],
13552            axis: axis_id,
13553        });
13554        let (src_delta, scene_delta) = frontend
13555            .add_constraint(&ctx, version, sketch_id, constraint)
13556            .await
13557            .unwrap();
13558        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
13559        assert_eq!(
13560            scene_delta.new_graph.objects.len(),
13561            14,
13562            "{:#?}",
13563            scene_delta.new_graph.objects
13564        );
13565
13566        ctx.close().await;
13567    }
13568
13569    #[tokio::test(flavor = "multi_thread")]
13570    async fn test_sketch_on_face_simple() {
13571        let initial_source = "\
13572len = 2mm
13573cube = startSketchOn(XY)
13574  |> startProfile(at = [0, 0])
13575  |> line(end = [len, 0], tag = $side)
13576  |> line(end = [0, len])
13577  |> line(end = [-len, 0])
13578  |> line(end = [0, -len])
13579  |> close()
13580  |> extrude(length = len)
13581
13582face = faceOf(cube, face = side)
13583";
13584
13585        let program = Program::parse(initial_source).unwrap().0.unwrap();
13586
13587        let mut frontend = FrontendState::new();
13588
13589        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13590        let mock_ctx = ExecutorContext::new_mock(None).await;
13591        let version = Version(0);
13592
13593        frontend.hack_set_program(&ctx, program).await.unwrap();
13594        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13595        let face_id = face_object.id;
13596
13597        let sketch_args = SketchCtor {
13598            on: Plane::Object(face_id),
13599        };
13600        let (_src_delta, scene_delta, sketch_id) = frontend
13601            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13602            .await
13603            .unwrap();
13604        assert_eq!(sketch_id, ObjectId(2));
13605        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13606        let sketch_object = &scene_delta.new_graph.objects[2];
13607        assert_eq!(sketch_object.id, ObjectId(2));
13608        assert_eq!(
13609            sketch_object.kind,
13610            ObjectKind::Sketch(Sketch {
13611                args: SketchCtor {
13612                    on: Plane::Object(face_id),
13613                },
13614                plane: face_id,
13615                segments: vec![],
13616                constraints: vec![],
13617            })
13618        );
13619        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13620
13621        ctx.close().await;
13622        mock_ctx.close().await;
13623    }
13624
13625    #[tokio::test(flavor = "multi_thread")]
13626    async fn test_new_sketch_on_primitive_index_face() {
13627        let initial_source = "\
13628@settings(kclVersion = 2.0)
13629
13630sketch001 = sketch(on = XY) {
13631  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
13632}
13633extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
13634shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
13635        let program = Program::parse(initial_source).unwrap().0.unwrap();
13636        let ctx = ExecutorContext::new_mock(None).await;
13637        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13638        let solid_id = match outcome.variables.get("shell001") {
13639            Some(KclValueView::Solid { value }) => value.id,
13640            value => panic!("expected shell001 to be a solid, got {value:?}"),
13641        };
13642        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
13643
13644        let mut ast = program.ast;
13645        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
13646        let face_expr = sketch_on_ast_expr(
13647            &mut ast,
13648            &scene_graph,
13649            &solid_references,
13650            &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
13651        )
13652        .unwrap();
13653        let face_decl = ast::VariableDeclaration::new(
13654            ast::VariableDeclarator::new("face001", face_expr),
13655            ast::ItemVisibility::Default,
13656            ast::VariableKind::Const,
13657        );
13658        ast.body
13659            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13660                face_decl,
13661            ))));
13662        let face_source = source_from_ast(&ast);
13663        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
13664        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
13665
13666        let program = Program::parse(&new_source).unwrap().0.unwrap();
13667        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13668        ctx.close().await;
13669    }
13670
13671    #[tokio::test(flavor = "multi_thread")]
13672    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13673        let initial_source = "\
13674s = sketch(on = YZ) {
13675  line1 = line(start = [0, 0], end = [0, 1])
13676  line2 = line(start = [0, 1], end = [1, 1])
13677  line3 = line(start = [1, 1], end = [0, 0])
13678}
13679region001 = region(point = [0.1, 0.1], sketch = s)
13680extrude001 = extrude(region001, length = 5)
13681";
13682
13683        let program = Program::parse(initial_source).unwrap().0.unwrap();
13684
13685        let mut frontend = FrontendState::new();
13686        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13687        let version = Version(0);
13688
13689        frontend.hack_set_program(&ctx, program).await.unwrap();
13690        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13691
13692        let sketch_args = SketchCtor {
13693            on: Plane::Object(wall_object_id),
13694        };
13695        let (src_delta, _scene_delta, _sketch_id) = frontend
13696            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13697            .await
13698            .unwrap();
13699        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13700
13701        ctx.close().await;
13702    }
13703
13704    #[tokio::test(flavor = "multi_thread")]
13705    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13706        let initial_source = "\
13707sketch001 = sketch(on = YZ) {
13708  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13709  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13710  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13711  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13712  coincident([line1.end, line2.start])
13713  coincident([line2.end, line3.start])
13714  coincident([line3.end, line4.start])
13715  coincident([line4.end, line1.start])
13716  parallel([line2, line4])
13717  parallel([line3, line1])
13718  perpendicular([line1, line2])
13719  horizontal(line3)
13720  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13721}
13722region001 = region(point = [3.1, 3.74], sketch = sketch001)
13723extrude001 = extrude(region001, length = 5)
13724";
13725
13726        let program = Program::parse(initial_source).unwrap().0.unwrap();
13727
13728        let mut frontend = FrontendState::new();
13729        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13730        let version = Version(0);
13731
13732        frontend.hack_set_program(&ctx, program).await.unwrap();
13733        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13734
13735        let sketch_args = SketchCtor {
13736            on: Plane::Object(wall_object_id),
13737        };
13738        let (src_delta, _scene_delta, _sketch_id) = frontend
13739            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13740            .await
13741            .unwrap();
13742        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13743
13744        ctx.close().await;
13745    }
13746
13747    #[tokio::test(flavor = "multi_thread")]
13748    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13749        let initial_source = "\
13750@settings(kclVersion = 2.0)
13751
13752sketch001 = sketch(on = XY) {
13753  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13754  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13755}
13756hidden001 = hide(sketch001)
13757region001 = region(segments = [sketch001.circle2])
13758region002 = region(segments = [sketch001.circle1])
13759extrude001 = extrude([region001, region002], length = 5)
13760";
13761
13762        let program = Program::parse(initial_source).unwrap().0.unwrap();
13763        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13764        let version = Version(0);
13765
13766        for (solid_output_index, expected_face) in [
13767            (0, "faceOf(extrude001[0], face = END)"),
13768            (1, "faceOf(extrude001[1], face = END)"),
13769        ] {
13770            let mut frontend = FrontendState::new();
13771            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13772            let cap_object_id = find_cap_object_id_with_solid_output_index(
13773                &frontend.scene_graph,
13774                crate::frontend::api::CapKind::End,
13775                solid_output_index,
13776            )
13777            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13778
13779            let sketch_args = SketchCtor {
13780                on: Plane::Object(cap_object_id),
13781            };
13782            let (src_delta, _scene_delta, _sketch_id) = frontend
13783                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13784                .await
13785                .unwrap();
13786
13787            assert!(
13788                src_delta.text.contains(expected_face),
13789                "expected `{expected_face}` in:\n{}",
13790                src_delta.text
13791            );
13792            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13793        }
13794
13795        ctx.close().await;
13796    }
13797
13798    #[tokio::test(flavor = "multi_thread")]
13799    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13800        let initial_source = "\
13801@settings(kclVersion = 2.0)
13802
13803sketch001 = sketch(on = XY) {
13804  rect1Line1 = line(start = [0, 0], end = [1, 0])
13805  rect1Line2 = line(start = [1, 0], end = [1, 1])
13806  rect1Line3 = line(start = [1, 1], end = [0, 1])
13807  rect1Line4 = line(start = [0, 1], end = [0, 0])
13808  rect2Line1 = line(start = [3, 0], end = [4, 0])
13809  rect2Line2 = line(start = [4, 0], end = [4, 1])
13810  rect2Line3 = line(start = [4, 1], end = [3, 1])
13811  rect2Line4 = line(start = [3, 1], end = [3, 0])
13812}
13813hidden001 = hide(sketch001)
13814region001 = region(segments = [
13815  sketch001.rect1Line4,
13816  sketch001.rect1Line1
13817])
13818region002 = region(segments = [
13819  sketch001.rect2Line4,
13820  sketch001.rect2Line1
13821])
13822extrude001 = extrude([region001, region002], length = 5)
13823";
13824
13825        let program = Program::parse(initial_source).unwrap().0.unwrap();
13826        let mut frontend = FrontendState::new();
13827        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13828        let version = Version(0);
13829
13830        frontend.hack_set_program(&ctx, program).await.unwrap();
13831        let region_call = "\
13832region(segments = [
13833  sketch001.rect1Line4,
13834  sketch001.rect1Line1
13835])";
13836        let region_call_start = initial_source.find(region_call).unwrap();
13837        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
13838        let segment_call = "line(start = [0, 0], end = [1, 0])";
13839        let segment_call_start = initial_source.find(segment_call).unwrap();
13840        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
13841        let wall_object_id = frontend
13842            .scene_graph
13843            .objects
13844            .iter()
13845            .find_map(|object| match &object.kind {
13846                ObjectKind::Wall(wall)
13847                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
13848                        && wall.source.segment.range == segment_range =>
13849                {
13850                    Some(object.id)
13851                }
13852                _ => None,
13853            })
13854            .expect("expected a wall object for region001.tags.rect1Line1");
13855
13856        let sketch_args = SketchCtor {
13857            on: Plane::Object(wall_object_id),
13858        };
13859        let (src_delta, _scene_delta, _sketch_id) = frontend
13860            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13861            .await
13862            .unwrap();
13863
13864        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
13865        assert!(
13866            src_delta.text.contains(expected_face),
13867            "expected `{expected_face}` in:\n{}",
13868            src_delta.text
13869        );
13870        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
13871
13872        ctx.close().await;
13873    }
13874
13875    #[test]
13876    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13877        let source = "\
13878sketch001 = sketch(on = XY) {
13879  line(start = [0, 0], end = [1, 0])
13880}
13881part = subtract(boxSolid, tools = [cutSolid])
13882  |> appearance(color = \"#8f96a3\")
13883";
13884        let ast = Program::parse(source).unwrap().0.unwrap().ast;
13885        let line_start = source.find("line").unwrap();
13886        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13887        let line_ref = SourceRef::Simple {
13888            range: [line_start, line_end, 0].into(),
13889            node_path: None,
13890        };
13891        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13892
13893        let subtract_start = source.find("subtract").unwrap();
13894        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13895        let subtract_ref = SourceRef::Simple {
13896            range: [subtract_start, subtract_end, 0].into(),
13897            node_path: None,
13898        };
13899        assert_eq!(
13900            variable_name_containing_source_ref(&ast, &subtract_ref),
13901            Some("part".to_owned())
13902        );
13903    }
13904
13905    #[tokio::test(flavor = "multi_thread")]
13906    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13907        clear_mem_cache().await;
13908        let source = "\
13909boxSolid = startSketchOn(XY)
13910  |> startProfile(at = [0, 0])
13911  |> line(end = [4, 0], tag = $bottomEdge)
13912  |> line(end = [0, 4])
13913  |> line(end = [-4, 0])
13914  |> close()
13915  |> extrude(length = 10)
13916cutSolid = startSketchOn(XY)
13917  |> startProfile(at = [1, 1])
13918  |> line(end = [1, 0])
13919  |> line(end = [0, 1])
13920  |> line(end = [-1, 0])
13921  |> close()
13922  |> extrude(length = 10)
13923part = subtract(boxSolid, tools = [cutSolid])
13924  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13925";
13926        let program = Program::parse(source).unwrap().0.unwrap();
13927        let mut frontend = FrontendState::new();
13928        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13929        match frontend.hack_set_program(&ctx, program).await.unwrap() {
13930            SetProgramOutcome::Success { .. } => {}
13931            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13932        }
13933
13934        let sweep_call_start = source.find("extrude").unwrap();
13935        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13936        let part_call_start = source.find("subtract").unwrap();
13937        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13938        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13939        let composite_range = [part_call_start, part_call_end, 0].into();
13940
13941        let cap_object = frontend
13942            .scene_graph
13943            .objects
13944            .iter()
13945            .find(|object| {
13946                matches!(
13947                    &object.kind,
13948                    ObjectKind::Cap(crate::frontend::api::Cap {
13949                        kind: crate::frontend::api::CapKind::End,
13950                        source,
13951                        ..
13952                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
13953                )
13954            })
13955            .expect("expected end cap object to trace through subtract and original extrude");
13956
13957        let mut ast = frontend.program.ast.clone();
13958        let cap_expr = sketch_on_ast_expr(
13959            &mut ast,
13960            &frontend.scene_graph,
13961            &frontend.solid_references,
13962            &Plane::Object(cap_object.id),
13963        )
13964        .unwrap();
13965        let cap_face_decl = ast::VariableDeclaration::new(
13966            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13967            ast::ItemVisibility::Default,
13968            ast::VariableKind::Const,
13969        );
13970        ast.body
13971            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13972                cap_face_decl,
13973            ))));
13974        let generated_source = source_from_ast(&ast);
13975
13976        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13977        assert!(!generated_source.contains("faceOf(boxSolid"));
13978        let ast::Expr::CallExpressionKw(call) = cap_expr else {
13979            panic!("expected faceOf call");
13980        };
13981        assert_eq!(call.callee.name.name, "faceOf");
13982        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13983            panic!("expected solid name");
13984        };
13985        assert_eq!(solid_name.name.name, "part");
13986        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13987            panic!("expected face name");
13988        };
13989        assert_eq!(face_name.name.name, "END");
13990
13991        ctx.close().await;
13992    }
13993
13994    #[tokio::test(flavor = "multi_thread")]
13995    async fn test_sketch_on_plane_incremental() {
13996        let initial_source = "\
13997len = 2mm
13998cube = startSketchOn(XY)
13999  |> startProfile(at = [0, 0])
14000  |> line(end = [len, 0], tag = $side)
14001  |> line(end = [0, len])
14002  |> line(end = [-len, 0])
14003  |> line(end = [0, -len])
14004  |> close()
14005  |> extrude(length = len)
14006
14007plane = planeOf(cube, face = side)
14008";
14009
14010        let program = Program::parse(initial_source).unwrap().0.unwrap();
14011
14012        let mut frontend = FrontendState::new();
14013
14014        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14015        let mock_ctx = ExecutorContext::new_mock(None).await;
14016        let version = Version(0);
14017
14018        frontend.hack_set_program(&ctx, program).await.unwrap();
14019        // Find the last plane since the first plane is the XY plane.
14020        let plane_object = frontend
14021            .scene_graph
14022            .objects
14023            .iter()
14024            .rev()
14025            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14026            .unwrap();
14027        let plane_id = plane_object.id;
14028
14029        let sketch_args = SketchCtor {
14030            on: Plane::Object(plane_id),
14031        };
14032        let (src_delta, scene_delta, sketch_id) = frontend
14033            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14034            .await
14035            .unwrap();
14036        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14037        assert_eq!(sketch_id, ObjectId(2));
14038        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14039        let sketch_object = &scene_delta.new_graph.objects[2];
14040        assert_eq!(sketch_object.id, ObjectId(2));
14041        assert_eq!(
14042            sketch_object.kind,
14043            ObjectKind::Sketch(Sketch {
14044                args: SketchCtor {
14045                    on: Plane::Object(plane_id),
14046                },
14047                plane: plane_id,
14048                segments: vec![],
14049                constraints: vec![],
14050            })
14051        );
14052        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14053
14054        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14055        assert_eq!(plane_object.id, plane_id);
14056        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14057
14058        ctx.close().await;
14059        mock_ctx.close().await;
14060    }
14061
14062    #[tokio::test(flavor = "multi_thread")]
14063    async fn test_new_sketch_uses_unique_variable_name() {
14064        let initial_source = "\
14065sketch1 = sketch(on = XY) {
14066}
14067";
14068
14069        let program = Program::parse(initial_source).unwrap().0.unwrap();
14070
14071        let mut frontend = FrontendState::new();
14072        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14073        let version = Version(0);
14074
14075        frontend.hack_set_program(&ctx, program).await.unwrap();
14076
14077        let sketch_args = SketchCtor {
14078            on: Plane::Default(PlaneName::Yz),
14079        };
14080        let (src_delta, _, _) = frontend
14081            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14082            .await
14083            .unwrap();
14084
14085        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14086
14087        ctx.close().await;
14088    }
14089
14090    #[tokio::test(flavor = "multi_thread")]
14091    async fn test_new_sketch_twice_using_same_plane() {
14092        let initial_source = "\
14093sketch1 = sketch(on = XY) {
14094}
14095";
14096
14097        let program = Program::parse(initial_source).unwrap().0.unwrap();
14098
14099        let mut frontend = FrontendState::new();
14100        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14101        let version = Version(0);
14102
14103        frontend.hack_set_program(&ctx, program).await.unwrap();
14104
14105        let sketch_args = SketchCtor {
14106            on: Plane::Default(PlaneName::Xy),
14107        };
14108        let (src_delta, _, _) = frontend
14109            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14110            .await
14111            .unwrap();
14112
14113        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14114
14115        ctx.close().await;
14116    }
14117
14118    #[tokio::test(flavor = "multi_thread")]
14119    async fn test_sketch_mode_reuses_cached_on_expression() {
14120        let initial_source = "\
14121width = 2mm
14122sketch(on = offsetPlane(XY, offset = width)) {
14123  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14124  distance([line1.start, line1.end]) == width
14125}
14126";
14127        let program = Program::parse(initial_source).unwrap().0.unwrap();
14128
14129        let mut frontend = FrontendState::new();
14130        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14131        let mock_ctx = ExecutorContext::new_mock(None).await;
14132        let version = Version(0);
14133        let project_id = ProjectId(0);
14134        let file_id = FileId(0);
14135
14136        frontend.hack_set_program(&ctx, program).await.unwrap();
14137        let initial_object_count = frontend.scene_graph.objects.len();
14138        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14139            .expect("Expected sketch object to exist")
14140            .id;
14141
14142        // Entering sketch mode should reuse cached `on` expression state
14143        // (offsetPlane result), not fail or create extra on-surface objects.
14144        let scene_delta = frontend
14145            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14146            .await
14147            .unwrap();
14148        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14149
14150        // A follow-up sketch-mode execution should keep the same stable object
14151        // graph shape as well.
14152        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14153        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14154
14155        ctx.close().await;
14156        mock_ctx.close().await;
14157    }
14158
14159    #[tokio::test(flavor = "multi_thread")]
14160    async fn test_multiple_sketch_blocks() {
14161        let initial_source = "\
14162// Cube that requires the engine.
14163width = 2
14164sketch001 = startSketchOn(XY)
14165profile001 = startProfile(sketch001, at = [0, 0])
14166  |> yLine(length = width, tag = $seg1)
14167  |> xLine(length = width)
14168  |> yLine(length = -width)
14169  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14170  |> close()
14171extrude001 = extrude(profile001, length = width)
14172
14173// Get a value that requires the engine.
14174x = segLen(seg1)
14175
14176// Triangle with side length 2*x.
14177sketch(on = XY) {
14178  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14179  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14180  coincident([line1.end, line2.start])
14181  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14182  coincident([line2.end, line3.start])
14183  coincident([line3.end, line1.start])
14184  equalLength([line3, line1])
14185  equalLength([line1, line2])
14186  distance([line1.start, line1.end]) == 2*x
14187}
14188
14189// Line segment with length x.
14190sketch2 = sketch(on = XY) {
14191  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14192  distance([line1.start, line1.end]) == x
14193}
14194";
14195
14196        let program = Program::parse(initial_source).unwrap().0.unwrap();
14197
14198        let mut frontend = FrontendState::new();
14199
14200        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14201        let mock_ctx = ExecutorContext::new_mock(None).await;
14202        let version = Version(0);
14203        let project_id = ProjectId(0);
14204        let file_id = FileId(0);
14205
14206        frontend.hack_set_program(&ctx, program).await.unwrap();
14207        let sketch_objects = frontend
14208            .scene_graph
14209            .objects
14210            .iter()
14211            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14212            .collect::<Vec<_>>();
14213        let sketch1_id = sketch_objects.first().unwrap().id;
14214        let sketch2_id = sketch_objects.get(1).unwrap().id;
14215        // First point in sketch1.
14216        let point1_id = ObjectId(sketch1_id.0 + 1);
14217        // First point in sketch2.
14218        let point2_id = ObjectId(sketch2_id.0 + 1);
14219
14220        // Edit the first sketch. Objects before the sketch block should be
14221        // present from execution cache so that we can sketch on prior planes,
14222        // for example. Objects after the first sketch block should not be
14223        // present since those statements are skipped in sketch mode.
14224        //
14225        // - startSketchOn(XY) Plane 1
14226        // - sketch on=XY Plane 1
14227        // - Sketch block 16
14228        let scene_delta = frontend
14229            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14230            .await
14231            .unwrap();
14232        assert_eq!(
14233            scene_delta.new_graph.objects.len(),
14234            18,
14235            "{:#?}",
14236            scene_delta.new_graph.objects
14237        );
14238
14239        // Edit a point in the first sketch.
14240        let point_ctor = PointCtor {
14241            position: Point2d {
14242                x: Expr::Var(Number {
14243                    value: 1.0,
14244                    units: NumericSuffix::Mm,
14245                }),
14246                y: Expr::Var(Number {
14247                    value: 2.0,
14248                    units: NumericSuffix::Mm,
14249                }),
14250            },
14251        };
14252        let segments = vec![ExistingSegmentCtor {
14253            id: point1_id,
14254            ctor: SegmentCtor::Point(point_ctor),
14255        }];
14256        let (src_delta, _) = frontend
14257            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14258            .await
14259            .unwrap();
14260        // Only the first sketch block changes.
14261        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
14262        let edited_sketch1_source = src_delta.text.clone();
14263
14264        // Execute mock to simulate drag end.
14265        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14266        assert_eq!(src_delta.text, edited_sketch1_source);
14267        // Exit sketch. Objects from the entire program should be present.
14268        //
14269        // - startSketchOn(XY) Plane 1
14270        // - sketch on=XY Plane 1
14271        // - Sketch block 16
14272        // - sketch on=XY cached
14273        // - Sketch block 5
14274        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14275        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14276
14277        // Edit the second sketch.
14278        //
14279        // - startSketchOn(XY) Plane 1
14280        // - sketch on=XY Plane 1
14281        // - Sketch block 16
14282        // - sketch on=XY cached
14283        // - Sketch block 5
14284        let scene_delta = frontend
14285            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14286            .await
14287            .unwrap();
14288        assert_eq!(
14289            scene_delta.new_graph.objects.len(),
14290            24,
14291            "{:#?}",
14292            scene_delta.new_graph.objects
14293        );
14294
14295        // Edit a point in the second sketch.
14296        let point_ctor = PointCtor {
14297            position: Point2d {
14298                x: Expr::Var(Number {
14299                    value: 3.0,
14300                    units: NumericSuffix::Mm,
14301                }),
14302                y: Expr::Var(Number {
14303                    value: 4.0,
14304                    units: NumericSuffix::Mm,
14305                }),
14306            },
14307        };
14308        let segments = vec![ExistingSegmentCtor {
14309            id: point2_id,
14310            ctor: SegmentCtor::Point(point_ctor),
14311        }];
14312        let (src_delta, _) = frontend
14313            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14314            .await
14315            .unwrap();
14316        // Only the second sketch block changes.
14317        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
14318        let edited_sketch2_source = src_delta.text.clone();
14319
14320        // Execute mock to simulate drag end.
14321        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14322        assert_eq!(src_delta.text, edited_sketch2_source);
14323
14324        ctx.close().await;
14325        mock_ctx.close().await;
14326    }
14327
14328    #[tokio::test(flavor = "multi_thread")]
14329    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14330        clear_mem_cache().await;
14331
14332        let source = r#"sketch001 = sketch(on = XZ) {
14333  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14334}
14335sketch002 = sketch(on = XY) {
14336  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14337  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14338  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14339  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14340  coincident([line1.end, line2.start])
14341  coincident([line2.end, line3.start])
14342  coincident([line3.end, line4.start])
14343  coincident([line4.end, line1.start])
14344  parallel([line2, line4])
14345  parallel([line3, line1])
14346  perpendicular([line1, line2])
14347  horizontal(line3)
14348  coincident([line1.start, ORIGIN])
14349}
14350"#;
14351
14352        let program = Program::parse(source).unwrap().0.unwrap();
14353        let mut frontend = FrontendState::new();
14354        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14355        let mock_ctx = ExecutorContext::new_mock(None).await;
14356        let version = Version(0);
14357        let project_id = ProjectId(0);
14358        let file_id = FileId(0);
14359
14360        frontend.hack_set_program(&ctx, program).await.unwrap();
14361        let sketch_objects = frontend
14362            .scene_graph
14363            .objects
14364            .iter()
14365            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14366            .collect::<Vec<_>>();
14367        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14368
14369        let sketch1_id = sketch_objects[0].id;
14370        let sketch2_id = sketch_objects[1].id;
14371
14372        frontend
14373            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14374            .await
14375            .unwrap();
14376        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14377
14378        let scene_delta = frontend
14379            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14380            .await
14381            .unwrap();
14382        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14383
14384        clear_mem_cache().await;
14385        ctx.close().await;
14386        mock_ctx.close().await;
14387    }
14388
14389    // Regression tests: operations on source code with extra whitespace/newlines.
14390    // These test that NodePath-based lookups work correctly when source ranges
14391    // are shifted by extra whitespace that wouldn't be present after formatting.
14392
14393    #[tokio::test(flavor = "multi_thread")]
14394    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14395        // Extra newlines after @settings line - this shifts all source ranges.
14396        let initial_source = "@settings(defaultLengthUnit = mm)
14397
14398sketch001 = sketch(on = XY) {
14399  point(at = [1in, 2in])
14400}
14401";
14402
14403        let program = Program::parse(initial_source).unwrap().0.unwrap();
14404        let mut frontend = FrontendState::new();
14405
14406        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14407        let mock_ctx = ExecutorContext::new_mock(None).await;
14408        let version = Version(0);
14409        let project_id = ProjectId(0);
14410        let file_id = FileId(0);
14411
14412        frontend.hack_set_program(&ctx, program).await.unwrap();
14413        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14414        let sketch_id = sketch_object.id;
14415
14416        // Edit sketch should succeed despite extra newlines.
14417        frontend
14418            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14419            .await
14420            .unwrap();
14421
14422        // Add a new point to the sketch.
14423        let point_ctor = PointCtor {
14424            position: Point2d {
14425                x: Expr::Number(Number {
14426                    value: 5.0,
14427                    units: NumericSuffix::Mm,
14428                }),
14429                y: Expr::Number(Number {
14430                    value: 6.0,
14431                    units: NumericSuffix::Mm,
14432                }),
14433            },
14434        };
14435        let segment = SegmentCtor::Point(point_ctor);
14436        let (src_delta, scene_delta) = frontend
14437            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14438            .await
14439            .unwrap();
14440        // After adding a point, the source should be reformatted with standard whitespace.
14441        assert!(
14442            src_delta.text.contains("point(at = [5mm, 6mm])"),
14443            "Expected new point in source, got: {}",
14444            src_delta.text
14445        );
14446        assert!(!scene_delta.new_objects.is_empty());
14447
14448        ctx.close().await;
14449        mock_ctx.close().await;
14450    }
14451
14452    #[tokio::test(flavor = "multi_thread")]
14453    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14454        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14455
14456        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14457        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14458
14459        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14460        assert!(
14461            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14462            "Expected experimental settings to be added to source"
14463        );
14464    }
14465
14466    #[tokio::test(flavor = "multi_thread")]
14467    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14468        // Extra newlines after @settings, with an empty sketch block.
14469        let initial_source = "@settings(defaultLengthUnit = mm)
14470
14471s = sketch(on = XY) {}
14472";
14473
14474        let program = Program::parse(initial_source).unwrap().0.unwrap();
14475        let mut frontend = FrontendState::new();
14476
14477        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14478        let mock_ctx = ExecutorContext::new_mock(None).await;
14479        let version = Version(0);
14480
14481        frontend.hack_set_program(&ctx, program).await.unwrap();
14482        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14483        let sketch_id = sketch_object.id;
14484
14485        let line_ctor = LineCtor {
14486            start: Point2d {
14487                x: Expr::Number(Number {
14488                    value: 0.0,
14489                    units: NumericSuffix::Mm,
14490                }),
14491                y: Expr::Number(Number {
14492                    value: 0.0,
14493                    units: NumericSuffix::Mm,
14494                }),
14495            },
14496            end: Point2d {
14497                x: Expr::Number(Number {
14498                    value: 10.0,
14499                    units: NumericSuffix::Mm,
14500                }),
14501                y: Expr::Number(Number {
14502                    value: 10.0,
14503                    units: NumericSuffix::Mm,
14504                }),
14505            },
14506            construction: None,
14507        };
14508        let segment = SegmentCtor::Line(line_ctor);
14509        let (src_delta, scene_delta) = frontend
14510            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14511            .await
14512            .unwrap();
14513        assert!(
14514            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14515            "Expected line in source, got: {}",
14516            src_delta.text
14517        );
14518        // Line creates start point, end point, and line segment.
14519        assert_eq!(scene_delta.new_objects.len(), 3);
14520
14521        ctx.close().await;
14522        mock_ctx.close().await;
14523    }
14524
14525    #[tokio::test(flavor = "multi_thread")]
14526    async fn test_extra_newlines_between_operations_edit_line() {
14527        // Extra newlines between @settings and sketch, and inside the sketch block.
14528        let initial_source = "@settings(defaultLengthUnit = mm)
14529
14530sketch001 = sketch(on = XY) {
14531
14532  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14533
14534}
14535";
14536
14537        let program = Program::parse(initial_source).unwrap().0.unwrap();
14538        let mut frontend = FrontendState::new();
14539
14540        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14541        let mock_ctx = ExecutorContext::new_mock(None).await;
14542        let version = Version(0);
14543        let project_id = ProjectId(0);
14544        let file_id = FileId(0);
14545
14546        frontend.hack_set_program(&ctx, program).await.unwrap();
14547        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14548        let sketch_id = sketch_object.id;
14549        let sketch = expect_sketch(sketch_object);
14550
14551        // Extract segment IDs before edit_sketch borrows frontend mutably.
14552        let line_id = sketch
14553            .segments
14554            .iter()
14555            .copied()
14556            .find(|seg_id| {
14557                matches!(
14558                    &frontend.scene_graph.objects[seg_id.0].kind,
14559                    ObjectKind::Segment {
14560                        segment: Segment::Line(_)
14561                    }
14562                )
14563            })
14564            .expect("Expected a line segment in sketch");
14565
14566        // Enter sketch edit mode.
14567        frontend
14568            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14569            .await
14570            .unwrap();
14571
14572        // Edit the line.
14573        let line_ctor = LineCtor {
14574            start: Point2d {
14575                x: Expr::Var(Number {
14576                    value: 1.0,
14577                    units: NumericSuffix::Mm,
14578                }),
14579                y: Expr::Var(Number {
14580                    value: 2.0,
14581                    units: NumericSuffix::Mm,
14582                }),
14583            },
14584            end: Point2d {
14585                x: Expr::Var(Number {
14586                    value: 13.0,
14587                    units: NumericSuffix::Mm,
14588                }),
14589                y: Expr::Var(Number {
14590                    value: 14.0,
14591                    units: NumericSuffix::Mm,
14592                }),
14593            },
14594            construction: None,
14595        };
14596        let segments = vec![ExistingSegmentCtor {
14597            id: line_id,
14598            ctor: SegmentCtor::Line(line_ctor),
14599        }];
14600        let (src_delta, _scene_delta) = frontend
14601            .edit_segments(&mock_ctx, version, sketch_id, segments)
14602            .await
14603            .unwrap();
14604        assert!(
14605            src_delta
14606                .text
14607                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14608            "Expected edited line in source, got: {}",
14609            src_delta.text
14610        );
14611
14612        ctx.close().await;
14613        mock_ctx.close().await;
14614    }
14615
14616    #[tokio::test(flavor = "multi_thread")]
14617    async fn test_extra_newlines_delete_segment() {
14618        // Extra whitespace before and after the sketch block.
14619        let initial_source = "@settings(defaultLengthUnit = mm)
14620
14621sketch001 = sketch(on = XY) {
14622  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14623}
14624";
14625
14626        let program = Program::parse(initial_source).unwrap().0.unwrap();
14627        let mut frontend = FrontendState::new();
14628
14629        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14630        let mock_ctx = ExecutorContext::new_mock(None).await;
14631        let version = Version(0);
14632
14633        frontend.hack_set_program(&ctx, program).await.unwrap();
14634        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14635        let sketch_id = sketch_object.id;
14636        let sketch = expect_sketch(sketch_object);
14637
14638        // The sketch should have 3 segments: start point, center point, and the circle.
14639        assert_eq!(sketch.segments.len(), 3);
14640        let circle_id = sketch.segments[2];
14641
14642        // Delete the circle despite extra newlines in original source.
14643        let (src_delta, scene_delta) = frontend
14644            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14645            .await
14646            .unwrap();
14647        assert!(
14648            src_delta.text.contains("sketch(on = XY) {"),
14649            "Expected sketch block in source, got: {}",
14650            src_delta.text
14651        );
14652        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14653        let new_sketch = expect_sketch(new_sketch_object);
14654        assert_eq!(new_sketch.segments.len(), 0);
14655
14656        ctx.close().await;
14657        mock_ctx.close().await;
14658    }
14659
14660    #[tokio::test(flavor = "multi_thread")]
14661    async fn test_unformatted_source_add_arc() {
14662        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14663        let initial_source = "@settings(defaultLengthUnit = mm)
14664
14665sketch001 = sketch(on = XY) {
14666}
14667";
14668
14669        let program = Program::parse(initial_source).unwrap().0.unwrap();
14670        let mut frontend = FrontendState::new();
14671
14672        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14673        let mock_ctx = ExecutorContext::new_mock(None).await;
14674        let version = Version(0);
14675
14676        frontend.hack_set_program(&ctx, program).await.unwrap();
14677        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14678        let sketch_id = sketch_object.id;
14679
14680        let arc_ctor = ArcCtor {
14681            start: Point2d {
14682                x: Expr::Var(Number {
14683                    value: 5.0,
14684                    units: NumericSuffix::Mm,
14685                }),
14686                y: Expr::Var(Number {
14687                    value: 0.0,
14688                    units: NumericSuffix::Mm,
14689                }),
14690            },
14691            end: Point2d {
14692                x: Expr::Var(Number {
14693                    value: 0.0,
14694                    units: NumericSuffix::Mm,
14695                }),
14696                y: Expr::Var(Number {
14697                    value: 5.0,
14698                    units: NumericSuffix::Mm,
14699                }),
14700            },
14701            center: Point2d {
14702                x: Expr::Var(Number {
14703                    value: 0.0,
14704                    units: NumericSuffix::Mm,
14705                }),
14706                y: Expr::Var(Number {
14707                    value: 0.0,
14708                    units: NumericSuffix::Mm,
14709                }),
14710            },
14711            construction: None,
14712        };
14713        let segment = SegmentCtor::Arc(arc_ctor);
14714        let (src_delta, scene_delta) = frontend
14715            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14716            .await
14717            .unwrap();
14718        assert!(
14719            src_delta
14720                .text
14721                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14722            "Expected arc in source, got: {}",
14723            src_delta.text
14724        );
14725        assert!(!scene_delta.new_objects.is_empty());
14726
14727        ctx.close().await;
14728        mock_ctx.close().await;
14729    }
14730
14731    #[tokio::test(flavor = "multi_thread")]
14732    async fn test_extra_newlines_add_circle() {
14733        // Extra blank lines between settings and sketch.
14734        let initial_source = "@settings(defaultLengthUnit = mm)
14735
14736sketch001 = sketch(on = XY) {
14737}
14738";
14739
14740        let program = Program::parse(initial_source).unwrap().0.unwrap();
14741        let mut frontend = FrontendState::new();
14742
14743        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14744        let mock_ctx = ExecutorContext::new_mock(None).await;
14745        let version = Version(0);
14746
14747        frontend.hack_set_program(&ctx, program).await.unwrap();
14748        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14749        let sketch_id = sketch_object.id;
14750
14751        let circle_ctor = CircleCtor {
14752            start: Point2d {
14753                x: Expr::Var(Number {
14754                    value: 5.0,
14755                    units: NumericSuffix::Mm,
14756                }),
14757                y: Expr::Var(Number {
14758                    value: 0.0,
14759                    units: NumericSuffix::Mm,
14760                }),
14761            },
14762            center: Point2d {
14763                x: Expr::Var(Number {
14764                    value: 0.0,
14765                    units: NumericSuffix::Mm,
14766                }),
14767                y: Expr::Var(Number {
14768                    value: 0.0,
14769                    units: NumericSuffix::Mm,
14770                }),
14771            },
14772            construction: None,
14773        };
14774        let segment = SegmentCtor::Circle(circle_ctor);
14775        let (src_delta, scene_delta) = frontend
14776            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14777            .await
14778            .unwrap();
14779        assert!(
14780            src_delta
14781                .text
14782                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14783            "Expected circle in source, got: {}",
14784            src_delta.text
14785        );
14786        assert!(!scene_delta.new_objects.is_empty());
14787
14788        ctx.close().await;
14789        mock_ctx.close().await;
14790    }
14791
14792    #[tokio::test(flavor = "multi_thread")]
14793    async fn test_extra_newlines_add_constraint() {
14794        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14795        let initial_source = "@settings(defaultLengthUnit = mm)
14796
14797sketch001 = sketch(on = XY) {
14798  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14799  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14800}
14801";
14802
14803        let program = Program::parse(initial_source).unwrap().0.unwrap();
14804        let mut frontend = FrontendState::new();
14805
14806        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14807        let mock_ctx = ExecutorContext::new_mock(None).await;
14808        let version = Version(0);
14809        let project_id = ProjectId(0);
14810        let file_id = FileId(0);
14811
14812        frontend.hack_set_program(&ctx, program).await.unwrap();
14813        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14814        let sketch_id = sketch_object.id;
14815        let sketch = expect_sketch(sketch_object);
14816
14817        // Extract segment data before edit_sketch borrows frontend mutably.
14818        let line_ids: Vec<ObjectId> = sketch
14819            .segments
14820            .iter()
14821            .copied()
14822            .filter(|seg_id| {
14823                matches!(
14824                    &frontend.scene_graph.objects[seg_id.0].kind,
14825                    ObjectKind::Segment {
14826                        segment: Segment::Line(_)
14827                    }
14828                )
14829            })
14830            .collect();
14831        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14832
14833        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14834        let ObjectKind::Segment {
14835            segment: Segment::Line(line1_data),
14836        } = &line1.kind
14837        else {
14838            panic!("Expected line");
14839        };
14840        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14841        let ObjectKind::Segment {
14842            segment: Segment::Line(line2_data),
14843        } = &line2.kind
14844        else {
14845            panic!("Expected line");
14846        };
14847
14848        // Build constraint before entering sketch mode.
14849        let constraint = Constraint::Coincident(Coincident {
14850            segments: vec![line1_data.end.into(), line2_data.start.into()],
14851        });
14852
14853        // Enter sketch edit mode.
14854        frontend
14855            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14856            .await
14857            .unwrap();
14858        let (src_delta, _scene_delta) = frontend
14859            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14860            .await
14861            .unwrap();
14862        assert!(
14863            src_delta.text.contains("coincident("),
14864            "Expected coincident constraint in source, got: {}",
14865            src_delta.text
14866        );
14867
14868        ctx.close().await;
14869        mock_ctx.close().await;
14870    }
14871
14872    #[tokio::test(flavor = "multi_thread")]
14873    async fn test_extra_newlines_add_line_then_edit_line() {
14874        // Extra newlines after @settings - add a line, then edit it.
14875        let initial_source = "@settings(defaultLengthUnit = mm)
14876
14877sketch001 = sketch(on = XY) {
14878}
14879";
14880
14881        let program = Program::parse(initial_source).unwrap().0.unwrap();
14882        let mut frontend = FrontendState::new();
14883
14884        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14885        let mock_ctx = ExecutorContext::new_mock(None).await;
14886        let version = Version(0);
14887
14888        frontend.hack_set_program(&ctx, program).await.unwrap();
14889        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14890        let sketch_id = sketch_object.id;
14891
14892        // Add a line.
14893        let line_ctor = LineCtor {
14894            start: Point2d {
14895                x: Expr::Number(Number {
14896                    value: 0.0,
14897                    units: NumericSuffix::Mm,
14898                }),
14899                y: Expr::Number(Number {
14900                    value: 0.0,
14901                    units: NumericSuffix::Mm,
14902                }),
14903            },
14904            end: Point2d {
14905                x: Expr::Number(Number {
14906                    value: 10.0,
14907                    units: NumericSuffix::Mm,
14908                }),
14909                y: Expr::Number(Number {
14910                    value: 10.0,
14911                    units: NumericSuffix::Mm,
14912                }),
14913            },
14914            construction: None,
14915        };
14916        let segment = SegmentCtor::Line(line_ctor);
14917        let (src_delta, scene_delta) = frontend
14918            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14919            .await
14920            .unwrap();
14921        assert!(
14922            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14923            "Expected line in source after add, got: {}",
14924            src_delta.text
14925        );
14926        // Line creates start point, end point, and line segment.
14927        let line_id = *scene_delta.new_objects.last().unwrap();
14928
14929        // Edit the line.
14930        let line_ctor = LineCtor {
14931            start: Point2d {
14932                x: Expr::Number(Number {
14933                    value: 1.0,
14934                    units: NumericSuffix::Mm,
14935                }),
14936                y: Expr::Number(Number {
14937                    value: 2.0,
14938                    units: NumericSuffix::Mm,
14939                }),
14940            },
14941            end: Point2d {
14942                x: Expr::Number(Number {
14943                    value: 13.0,
14944                    units: NumericSuffix::Mm,
14945                }),
14946                y: Expr::Number(Number {
14947                    value: 14.0,
14948                    units: NumericSuffix::Mm,
14949                }),
14950            },
14951            construction: None,
14952        };
14953        let segments = vec![ExistingSegmentCtor {
14954            id: line_id,
14955            ctor: SegmentCtor::Line(line_ctor),
14956        }];
14957        let (src_delta, scene_delta) = frontend
14958            .edit_segments(&mock_ctx, version, sketch_id, segments)
14959            .await
14960            .unwrap();
14961        assert!(
14962            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14963            "Expected edited line in source, got: {}",
14964            src_delta.text
14965        );
14966        assert_eq!(scene_delta.new_objects, vec![]);
14967
14968        ctx.close().await;
14969        mock_ctx.close().await;
14970    }
14971}