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;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::SegmentDragAnchor;
19use crate::collections::AhashIndexSet;
20use crate::execution::Artifact;
21use crate::execution::ArtifactGraph;
22use crate::execution::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::CapSource;
53use crate::frontend::api::Expr;
54use crate::frontend::api::FileId;
55use crate::frontend::api::Number;
56use crate::frontend::api::ObjectId;
57use crate::frontend::api::ObjectKind;
58use crate::frontend::api::Plane;
59use crate::frontend::api::ProjectId;
60use crate::frontend::api::RestoreSketchCheckpointOutcome;
61use crate::frontend::api::SceneGraph;
62use crate::frontend::api::SceneGraphDelta;
63use crate::frontend::api::SketchCheckpointId;
64use crate::frontend::api::SourceDelta;
65use crate::frontend::api::SourceRef;
66use crate::frontend::api::SourceRefRange;
67use crate::frontend::api::Version;
68use crate::frontend::api::WallSource;
69use crate::frontend::modify::find_defined_names;
70use crate::frontend::modify::next_free_name;
71use crate::frontend::modify::next_free_name_with_padding;
72use crate::frontend::sketch::Coincident;
73use crate::frontend::sketch::Constraint;
74use crate::frontend::sketch::ConstraintSegment;
75use crate::frontend::sketch::Diameter;
76use crate::frontend::sketch::ExistingSegmentCtor;
77use crate::frontend::sketch::Horizontal;
78use crate::frontend::sketch::LineCtor;
79use crate::frontend::sketch::Point2d;
80use crate::frontend::sketch::Radius;
81use crate::frontend::sketch::Segment;
82use crate::frontend::sketch::SegmentCtor;
83use crate::frontend::sketch::SketchApi;
84use crate::frontend::sketch::SketchCtor;
85use crate::frontend::sketch::Vertical;
86use crate::frontend::traverse::MutateBodyItem;
87use crate::frontend::traverse::TraversalReturn;
88use crate::frontend::traverse::Visitor;
89use crate::frontend::traverse::dfs_mut;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::NodePathExt;
93use crate::pretty::NumericSuffix;
94use crate::std::constraints::LinesAtAngleKind;
95use crate::walk::NodeMut;
96use crate::walk::Visitable;
97
98pub(crate) mod api;
99pub(crate) mod modify;
100pub(crate) mod sketch;
101
102pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
103
104#[derive(Debug, Clone)]
105struct SketchCheckpoint {
106    id: SketchCheckpointId,
107    source: SourceDelta,
108    program: Program,
109    scene_graph: SceneGraph,
110    exec_outcome: ExecOutcome,
111    point_freedom_cache: HashMap<ObjectId, Freedom>,
112    mock_memory: Option<SketchModeState>,
113}
114mod traverse;
115pub(crate) mod trim;
116
117struct ArcSizeConstraintParams {
118    points: Vec<ObjectId>,
119    function_name: &'static str,
120    value: f64,
121    units: NumericSuffix,
122    label_position: Option<Point2d<Number>>,
123    constraint_type_name: &'static str,
124}
125
126const POINT_FN: &str = "point";
127const POINT_AT_PARAM: &str = "at";
128const LINE_FN: &str = "line";
129const LINE_VARIABLE: &str = "line";
130const LINE_START_PARAM: &str = "start";
131const LINE_END_PARAM: &str = "end";
132const ARC_FN: &str = "arc";
133const ARC_VARIABLE: &str = "arc";
134const ARC_START_PARAM: &str = "start";
135const ARC_END_PARAM: &str = "end";
136const ARC_CENTER_PARAM: &str = "center";
137const CIRCLE_FN: &str = "circle";
138const CIRCLE_VARIABLE: &str = "circle";
139const CIRCLE_START_PARAM: &str = "start";
140const CIRCLE_CENTER_PARAM: &str = "center";
141const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
142const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
143const LABEL_POSITION_PARAM: &str = "labelPosition";
144
145const COINCIDENT_FN: &str = "coincident";
146const DIAMETER_FN: &str = "diameter";
147const DISTANCE_FN: &str = "distance";
148const FIXED_FN: &str = "fixed";
149const ANGLE_FN: &str = "angle";
150const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
151const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
152const EQUAL_LENGTH_FN: &str = "equalLength";
153const EQUAL_RADIUS_FN: &str = "equalRadius";
154const HORIZONTAL_FN: &str = "horizontal";
155const MIDPOINT_FN: &str = "midpoint";
156const MIDPOINT_POINT_PARAM: &str = "point";
157const RADIUS_FN: &str = "radius";
158const SYMMETRIC_FN: &str = "symmetric";
159const SYMMETRIC_AXIS_PARAM: &str = "axis";
160const TANGENT_FN: &str = "tangent";
161const VERTICAL_FN: &str = "vertical";
162
163const LINE_PROPERTY_START: &str = "start";
164const LINE_PROPERTY_END: &str = "end";
165
166const ARC_PROPERTY_START: &str = "start";
167const ARC_PROPERTY_END: &str = "end";
168const ARC_PROPERTY_CENTER: &str = "center";
169const CIRCLE_PROPERTY_START: &str = "start";
170const CIRCLE_PROPERTY_CENTER: &str = "center";
171const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
172const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
173
174const CONSTRUCTION_PARAM: &str = "construction";
175
176#[derive(Debug, Clone, Copy)]
177enum EditDeleteKind {
178    Edit,
179    DeleteNonSketch,
180}
181
182/// Options that control how an edit is re-executed and written back.
183struct ExecuteAfterEditOptions {
184    segment_ids_edited: AhashIndexSet<ObjectId>,
185    edit_kind: EditDeleteKind,
186    commit_solved_initial_guesses: bool,
187}
188
189impl EditDeleteKind {
190    /// Returns true if this edit is any type of deletion.
191    fn is_delete(&self) -> bool {
192        match self {
193            EditDeleteKind::Edit => false,
194            EditDeleteKind::DeleteNonSketch => true,
195        }
196    }
197
198    fn to_change_kind(self) -> ChangeKind {
199        match self {
200            EditDeleteKind::Edit => ChangeKind::Edit,
201            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
202        }
203    }
204}
205
206#[derive(Debug, Clone, Copy)]
207enum ChangeKind {
208    Add,
209    Edit,
210    Delete,
211    None,
212}
213
214#[derive(Debug, Clone, Serialize, ts_rs::TS)]
215#[ts(export, export_to = "FrontendApi.ts")]
216#[serde(tag = "type")]
217pub enum SetProgramOutcome {
218    #[serde(rename_all = "camelCase")]
219    Success {
220        scene_graph: Box<SceneGraph>,
221        exec_outcome: Box<ExecOutcome>,
222        checkpoint_id: Option<SketchCheckpointId>,
223    },
224    #[serde(rename_all = "camelCase")]
225    ExecFailure { error: Box<KclErrorWithOutputs> },
226}
227
228/// Options for a sketch segment edit that participates in drag solving.
229pub struct EditSegmentsOptions {
230    /// Narrows which edited scene objects receive temporary fixed constraints.
231    ///
232    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
233    /// disables those fixed constraints, which is useful for semantic edits such
234    /// as toggling construction state.
235    pub anchor_segment_ids: Option<Vec<ObjectId>>,
236    /// Hidden fixed cursor points that the referenced segment bodies must pass
237    /// through during solve.
238    pub drag_anchors: Vec<SegmentDragAnchor>,
239    /// Whether solver-updated initial guesses should be written back to KCL.
240    pub commit_solved_initial_guesses: bool,
241}
242
243/// Options for a distance-constraint label edit during sketch dragging.
244pub struct EditDistanceConstraintLabelPositionOptions {
245    /// Edited scene objects to keep anchored while previewing the label edit.
246    pub anchor_segment_ids: Vec<ObjectId>,
247    /// Whether solver-updated initial guesses should be written back to KCL.
248    pub commit_solved_initial_guesses: bool,
249}
250
251#[derive(Debug, Clone)]
252pub struct FrontendState {
253    program: Program,
254    scene_graph: SceneGraph,
255    /// Stores the last known freedom value for each point object.
256    /// This allows us to preserve freedom values when freedom analysis isn't run.
257    point_freedom_cache: HashMap<ObjectId, Freedom>,
258    /// One-shot drag anchors for the next segment edit. These ids define which
259    /// edited points/segments become temporary fixed constraints during solve.
260    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
261    /// One-shot segment-body drag anchors for the next segment edit. These add
262    /// a temporary solver point on the dragged segment that follows the cursor.
263    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
264    /// One-shot override for whether the next edit commits solver-updated
265    /// initial guesses back into KCL. Drag previews keep this off so only the
266    /// explicit drag edit feeds the next solve.
267    next_edit_commits_solver_solutions: Option<bool>,
268    sketch_checkpoints: VecDeque<SketchCheckpoint>,
269    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
270}
271
272impl Default for FrontendState {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl FrontendState {
279    pub fn new() -> Self {
280        Self {
281            program: Program::empty(),
282            scene_graph: SceneGraph {
283                project: ProjectId(0),
284                file: FileId(0),
285                version: Version(0),
286                objects: Default::default(),
287                settings: Default::default(),
288                sketch_mode: Default::default(),
289            },
290            point_freedom_cache: HashMap::new(),
291            next_drag_anchor_segment_ids: None,
292            next_segment_drag_anchors: None,
293            next_edit_commits_solver_solutions: None,
294            sketch_checkpoints: VecDeque::new(),
295            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
296        }
297    }
298
299    /// Get a reference to the scene graph
300    pub fn scene_graph(&self) -> &SceneGraph {
301        &self.scene_graph
302    }
303
304    pub fn default_length_unit(&self) -> UnitLength {
305        self.program
306            .meta_settings()
307            .ok()
308            .flatten()
309            .map(|settings| settings.default_length_units)
310            .unwrap_or(UnitLength::Millimeters)
311    }
312
313    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
314        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
315
316        let checkpoint = SketchCheckpoint {
317            id: checkpoint_id,
318            source: SourceDelta {
319                text: source_from_ast(&self.program.ast),
320            },
321            program: self.program.clone(),
322            scene_graph: self.scene_graph.clone(),
323            exec_outcome,
324            point_freedom_cache: self.point_freedom_cache.clone(),
325            mock_memory: read_old_memory().await,
326        };
327
328        self.sketch_checkpoints.push_back(checkpoint);
329        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
330            self.sketch_checkpoints.pop_front();
331        }
332
333        Ok(checkpoint_id)
334    }
335
336    /// Edit sketch segments with optional drag-solve overrides.
337    ///
338    /// Drag anchors add hidden fixed cursor points and constrain the referenced
339    /// segment bodies to pass through them, which lets body drags use the same
340    /// anchor model without pinning all child points. Preview callers disable
341    /// solver writeback so solved geometry can be returned without feeding every
342    /// solver value back into KCL.
343    pub async fn edit_segments_with_options(
344        &mut self,
345        ctx: &ExecutorContext,
346        version: Version,
347        sketch: ObjectId,
348        segments: Vec<ExistingSegmentCtor>,
349        options: EditSegmentsOptions,
350    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
351        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
352            self.next_drag_anchor_segment_ids
353                .replace(anchor_ids.into_iter().collect())
354        });
355        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
356        let previous_commit_mode = self
357            .next_edit_commits_solver_solutions
358            .replace(options.commit_solved_initial_guesses);
359        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
360        if let Some(previous_anchor_ids) = previous_anchor_ids {
361            self.next_drag_anchor_segment_ids = previous_anchor_ids;
362        }
363        self.next_segment_drag_anchors = previous_drag_anchors;
364        self.next_edit_commits_solver_solutions = previous_commit_mode;
365        result
366    }
367
368    /// Edit a distance-constraint label position with optional solver writeback.
369    ///
370    /// Drag previews set `commit_solved_initial_guesses` to false so label
371    /// placement can be previewed against solved geometry without advancing
372    /// persistent KCL state until drag completion.
373    pub async fn edit_distance_constraint_label_position_with_options(
374        &mut self,
375        ctx: &ExecutorContext,
376        version: Version,
377        sketch: ObjectId,
378        constraint_id: ObjectId,
379        label_position: Point2d<Number>,
380        options: EditDistanceConstraintLabelPositionOptions,
381    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
382        let previous_commit_mode = self
383            .next_edit_commits_solver_solutions
384            .replace(options.commit_solved_initial_guesses);
385        let result = SketchApi::edit_distance_constraint_label_position(
386            self,
387            ctx,
388            version,
389            sketch,
390            constraint_id,
391            label_position,
392            options.anchor_segment_ids,
393        )
394        .await;
395        self.next_edit_commits_solver_solutions = previous_commit_mode;
396        result
397    }
398
399    pub async fn restore_sketch_checkpoint(
400        &mut self,
401        checkpoint_id: SketchCheckpointId,
402    ) -> api::Result<RestoreSketchCheckpointOutcome> {
403        let checkpoint = self
404            .sketch_checkpoints
405            .iter()
406            .find(|checkpoint| checkpoint.id == checkpoint_id)
407            .cloned()
408            .ok_or_else(|| Error {
409                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
410            })?;
411
412        self.program = checkpoint.program;
413        self.scene_graph = checkpoint.scene_graph.clone();
414        self.point_freedom_cache = checkpoint.point_freedom_cache;
415        self.next_drag_anchor_segment_ids = None;
416        self.next_segment_drag_anchors = None;
417        self.next_edit_commits_solver_solutions = None;
418
419        if let Some(mock_memory) = checkpoint.mock_memory {
420            write_old_memory(mock_memory).await;
421        } else {
422            clear_mem_cache().await;
423        }
424
425        Ok(RestoreSketchCheckpointOutcome {
426            source_delta: checkpoint.source,
427            scene_graph_delta: SceneGraphDelta {
428                new_graph: self.scene_graph_for_ui(),
429                new_objects: Vec::new(),
430                invalidates_ids: true,
431                exec_outcome: checkpoint.exec_outcome,
432            },
433        })
434    }
435
436    pub fn clear_sketch_checkpoints(&mut self) {
437        self.sketch_checkpoints.clear();
438    }
439    fn scene_graph_for_ui(&self) -> SceneGraph {
440        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
441            matches!(
442                object.kind,
443                ObjectKind::Segment {
444                    segment: Segment::ControlPointSpline(_)
445                }
446            )
447        });
448
449        if !has_control_point_splines {
450            return self.scene_graph.clone();
451        }
452
453        let hidden_constraint_ids = self
454            .scene_graph
455            .objects
456            .iter()
457            .filter_map(|object| match &object.kind {
458                ObjectKind::Constraint {
459                    constraint: Constraint::Coincident(coincident),
460                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
461                    Some(object.id)
462                }
463                _ => None,
464            })
465            .collect::<HashSet<_>>();
466
467        if hidden_constraint_ids.is_empty() {
468            return self.scene_graph.clone();
469        }
470
471        let mut scene_graph = self.scene_graph.clone();
472        for object in &mut scene_graph.objects {
473            match &mut object.kind {
474                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
475                    object.kind = ObjectKind::Nil;
476                }
477                ObjectKind::Sketch(sketch) => {
478                    sketch
479                        .constraints
480                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
481                }
482                _ => {}
483            }
484        }
485
486        scene_graph
487    }
488}
489
490fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
491    let mut first_owner_id = None;
492    for segment_id in coincident.segment_ids() {
493        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
494            return false;
495        };
496
497        match first_owner_id {
498            Some(first_owner_id) if first_owner_id != owner_id => return false,
499            Some(_) => {}
500            None => first_owner_id = Some(owner_id),
501        }
502    }
503
504    first_owner_id.is_some()
505}
506
507fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
508    let object = scene_graph.objects.get(segment_id.0)?;
509    let ObjectKind::Segment { segment } = &object.kind else {
510        return None;
511    };
512
513    match segment {
514        Segment::ControlPointSpline(_) => Some(segment_id),
515        Segment::Point(point) => point
516            .owner
517            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
518        Segment::Line(line) => line
519            .owner
520            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
521        _ => None,
522    }
523}
524
525fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
526    matches!(
527        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
528        Some(ObjectKind::Segment {
529            segment: Segment::ControlPointSpline(_)
530        })
531    )
532}
533
534fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
535    let experimental_features_allowed = program
536        .meta_settings()
537        .ok()
538        .flatten()
539        .map(|settings| settings.experimental_features == WarningLevel::Allow)
540        .unwrap_or(false);
541    if experimental_features_allowed {
542        return Ok(program.clone());
543    }
544
545    program.change_experimental_features(Some(WarningLevel::Allow))
546}
547
548impl SketchApi for FrontendState {
549    async fn execute_mock(
550        &mut self,
551        ctx: &ExecutorContext,
552        _version: Version,
553        sketch: ObjectId,
554    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
555        let sketch_block_ref =
556            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
557
558        let mut truncated_program = self.program.clone();
559        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
560            .map_err(KclErrorWithOutputs::no_outputs)?;
561
562        // Execute.
563        let outcome = ctx
564            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
565            .await?;
566        let new_source = source_from_ast(&self.program.ast);
567        let src_delta = SourceDelta { text: new_source };
568        // MockConfig::default() has freedom_analysis: true
569        let outcome = self.update_state_after_exec(outcome, true);
570        let scene_graph_delta = SceneGraphDelta {
571            new_graph: self.scene_graph.clone(),
572            new_objects: Default::default(),
573            invalidates_ids: false,
574            exec_outcome: outcome,
575        };
576        Ok((src_delta, scene_graph_delta))
577    }
578
579    async fn new_sketch(
580        &mut self,
581        ctx: &ExecutorContext,
582        _project: ProjectId,
583        _file: FileId,
584        _version: Version,
585        args: SketchCtor,
586    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
587        // TODO: Check version.
588
589        let mut new_ast = self.program.ast.clone();
590        // Create updated KCL source from args.
591        let mut plane_ast =
592            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
593        let mut defined_names = find_defined_names(&new_ast);
594        let is_face_of_expr = matches!(
595            &plane_ast,
596            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
597        );
598        if is_face_of_expr {
599            let face_name = next_free_name_with_padding("face", &defined_names)
600                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
601            let face_decl = ast::VariableDeclaration::new(
602                ast::VariableDeclarator::new(&face_name, plane_ast),
603                ast::ItemVisibility::Default,
604                ast::VariableKind::Const,
605            );
606            new_ast
607                .body
608                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
609                    face_decl,
610                ))));
611            defined_names.insert(face_name.clone());
612            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
613        }
614        let sketch_ast = ast::SketchBlock {
615            arguments: vec![ast::LabeledArg {
616                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
617                arg: plane_ast,
618            }],
619            body: Default::default(),
620            is_being_edited: false,
621            non_code_meta: Default::default(),
622            digest: None,
623        };
624        // Add a sketch block as a variable declaration directly, avoiding
625        // source-range mutation on a no-src node.
626        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
627            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
628        let sketch_decl = ast::VariableDeclaration::new(
629            ast::VariableDeclarator::new(
630                &sketch_name,
631                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
632            ),
633            ast::ItemVisibility::Default,
634            ast::VariableKind::Const,
635        );
636        new_ast
637            .body
638            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
639                sketch_decl,
640            ))));
641        // Convert to string source to create real source ranges.
642        let new_source = source_from_ast(&new_ast);
643        // Parse the new source.
644        let new_program = parse_frontend_mutation_source(
645            &new_source,
646            "Error parsing KCL source after adding sketch",
647            "No AST produced after adding sketch",
648        )?;
649
650        // Make sure to only set this if there are no errors.
651        self.program = new_program.clone();
652
653        // We need to do an engine execute so that the plane object gets created
654        // and is cached.
655        let outcome = ctx.run_with_caching(new_program.clone()).await?;
656        let freedom_analysis_ran = true;
657
658        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
659
660        let Some(sketch_id) = self
661            .scene_graph
662            .objects
663            .iter()
664            .filter_map(|object| match object.kind {
665                ObjectKind::Sketch(_) => Some(object.id),
666                _ => None,
667            })
668            .max_by_key(|id| id.0)
669        else {
670            return Err(KclErrorWithOutputs::from_error_outcome(
671                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
672                outcome,
673            ));
674        };
675        // Store the object in the scene.
676        self.scene_graph.sketch_mode = Some(sketch_id);
677
678        let src_delta = SourceDelta { text: new_source };
679        let scene_graph_delta = SceneGraphDelta {
680            new_graph: self.scene_graph_for_ui(),
681            invalidates_ids: false,
682            new_objects: vec![sketch_id],
683            exec_outcome: outcome,
684        };
685        Ok((src_delta, scene_graph_delta, sketch_id))
686    }
687
688    async fn edit_sketch(
689        &mut self,
690        ctx: &ExecutorContext,
691        _project: ProjectId,
692        _file: FileId,
693        _version: Version,
694        sketch: ObjectId,
695    ) -> ExecResult<SceneGraphDelta> {
696        // TODO: Check version.
697
698        // Look up existing sketch.
699        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
700            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
701        })?;
702        let ObjectKind::Sketch(_) = &sketch_object.kind else {
703            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
704                "Object is not a sketch, it is {}",
705                sketch_object.kind.human_friendly_kind_with_article()
706            ))));
707        };
708        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
709
710        // Enter sketch mode by setting the sketch_mode.
711        self.scene_graph.sketch_mode = Some(sketch);
712
713        // Truncate after the sketch block for mock execution.
714        let mut truncated_program = self.program.clone();
715        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
716            .map_err(KclErrorWithOutputs::no_outputs)?;
717
718        // Execute in mock mode to ensure state is up to date. The caller will
719        // want freedom analysis to display segments correctly.
720        let outcome = ctx
721            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
722            .await?;
723
724        // MockConfig::default() has freedom_analysis: true
725        let outcome = self.update_state_after_exec(outcome, true);
726        let scene_graph_delta = SceneGraphDelta {
727            new_graph: self.scene_graph_for_ui(),
728            invalidates_ids: false,
729            new_objects: Vec::new(),
730            exec_outcome: outcome,
731        };
732        Ok(scene_graph_delta)
733    }
734
735    async fn exit_sketch(
736        &mut self,
737        ctx: &ExecutorContext,
738        _version: Version,
739        sketch: ObjectId,
740    ) -> ExecResult<SceneGraph> {
741        // TODO: Check version.
742        #[cfg(not(target_arch = "wasm32"))]
743        let _ = sketch;
744        #[cfg(target_arch = "wasm32")]
745        if self.scene_graph.sketch_mode != Some(sketch) {
746            web_sys::console::warn_1(
747                &format!(
748                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
749                    self.scene_graph.sketch_mode
750                )
751                .into(),
752            );
753        }
754        self.scene_graph.sketch_mode = None;
755
756        // Execute.
757        let outcome = ctx.run_with_caching(self.program.clone()).await?;
758
759        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
760        self.update_state_after_exec(outcome, false);
761
762        Ok(self.scene_graph_for_ui())
763    }
764
765    async fn delete_sketch(
766        &mut self,
767        ctx: &ExecutorContext,
768        _version: Version,
769        sketch: ObjectId,
770    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
771        // TODO: Check version.
772
773        let mut new_ast = self.program.ast.clone();
774
775        // Look up existing sketch.
776        let sketch_id = sketch;
777        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
778            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
779        })?;
780        let ObjectKind::Sketch(_) = &sketch_object.kind else {
781            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
782                "Object is not a sketch, it is {}",
783                sketch_object.kind.human_friendly_kind_with_article(),
784            ))));
785        };
786
787        // Modify the AST to remove the sketch.
788        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
789            .map_err(KclErrorWithOutputs::no_outputs)?;
790
791        self.execute_after_delete_sketch(ctx, &mut new_ast).await
792    }
793
794    async fn add_segment(
795        &mut self,
796        ctx: &ExecutorContext,
797        _version: Version,
798        sketch: ObjectId,
799        segment: SegmentCtor,
800        _label: Option<String>,
801    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
802        // TODO: Check version.
803        match segment {
804            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
805            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
806            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
807            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
808            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
809        }
810    }
811
812    async fn edit_segments(
813        &mut self,
814        ctx: &ExecutorContext,
815        _version: Version,
816        sketch: ObjectId,
817        segments: Vec<ExistingSegmentCtor>,
818    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
819        // TODO: Check version.
820        let sketch_block_ref =
821            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
822
823        let mut new_ast = self.program.ast.clone();
824        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
825        let mut invalidates_ids = false;
826
827        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
828        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
829        for segment in &segments {
830            edited_segment_ids.insert(segment.id);
831            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
832                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
833                && let ObjectKind::Segment {
834                    segment: Segment::ControlPointSpline(existing_spline),
835                } = &existing_object.kind
836                && existing_spline.controls.len() != new_ctor.points.len()
837            {
838                invalidates_ids = true;
839            }
840        }
841        let drag_anchor_segment_ids = self
842            .next_drag_anchor_segment_ids
843            .take()
844            .unwrap_or_else(|| edited_segment_ids.clone());
845        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
846
847        // Preprocess segments into a final_edits vector to handle if segments contains:
848        // - edit start point of line1 (as SegmentCtor::Point)
849        // - edit end point of line1 (as SegmentCtor::Point)
850        //
851        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
852        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
853        //
854        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
855        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
856        // so the above example would result in a single line1 edit:
857        // - the first start point edit creates a new line edit entry in final_edits
858        // - the second end point edit finds this line edit and mutates the end position only.
859        //
860        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
861        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
862
863        for segment in segments {
864            let segment_id = segment.id;
865            match segment.ctor {
866                SegmentCtor::Point(ctor) => {
867                    // Find the owner, if any (point -> line / arc)
868                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
869                        && let ObjectKind::Segment { segment } = &segment_object.kind
870                        && let Segment::Point(point) = segment
871                        && let Some(owner_id) = point.owner
872                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
873                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
874                    {
875                        match owner_segment {
876                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
877                                if let Some(existing) = final_edits.get_mut(&owner_id) {
878                                    let SegmentCtor::Line(line_ctor) = existing else {
879                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
880                                            "Internal: Expected line ctor for owner, but found {}",
881                                            existing.human_friendly_kind_with_article()
882                                        ))));
883                                    };
884                                    // Line owner is already in final_edits -> apply this point edit
885                                    if line.start == segment_id {
886                                        line_ctor.start = ctor.position;
887                                    } else {
888                                        line_ctor.end = ctor.position;
889                                    }
890                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
891                                    // Line owner is not in final_edits yet -> create it
892                                    let mut line_ctor = line_ctor.clone();
893                                    if line.start == segment_id {
894                                        line_ctor.start = ctor.position;
895                                    } else {
896                                        line_ctor.end = ctor.position;
897                                    }
898                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
899                                } else {
900                                    // This should never run..
901                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
902                                        "Internal: Line does not have line ctor, but found {}",
903                                        line.ctor.human_friendly_kind_with_article()
904                                    ))));
905                                }
906                                continue;
907                            }
908                            Segment::Arc(arc)
909                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
910                            {
911                                if let Some(existing) = final_edits.get_mut(&owner_id) {
912                                    let SegmentCtor::Arc(arc_ctor) = existing else {
913                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
914                                            "Internal: Expected arc ctor for owner, but found {}",
915                                            existing.human_friendly_kind_with_article()
916                                        ))));
917                                    };
918                                    if arc.start == segment_id {
919                                        arc_ctor.start = ctor.position;
920                                    } else if arc.end == segment_id {
921                                        arc_ctor.end = ctor.position;
922                                    } else {
923                                        arc_ctor.center = ctor.position;
924                                    }
925                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
926                                    let mut arc_ctor = arc_ctor.clone();
927                                    if arc.start == segment_id {
928                                        arc_ctor.start = ctor.position;
929                                    } else if arc.end == segment_id {
930                                        arc_ctor.end = ctor.position;
931                                    } else {
932                                        arc_ctor.center = ctor.position;
933                                    }
934                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
935                                } else {
936                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
937                                        "Internal: Arc does not have arc ctor, but found {}",
938                                        arc.ctor.human_friendly_kind_with_article()
939                                    ))));
940                                }
941                                continue;
942                            }
943                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
944                                if let Some(existing) = final_edits.get_mut(&owner_id) {
945                                    let SegmentCtor::Circle(circle_ctor) = existing else {
946                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
947                                            "Internal: Expected circle ctor for owner, but found {}",
948                                            existing.human_friendly_kind_with_article()
949                                        ))));
950                                    };
951                                    if circle.start == segment_id {
952                                        circle_ctor.start = ctor.position;
953                                    } else {
954                                        circle_ctor.center = ctor.position;
955                                    }
956                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
957                                    let mut circle_ctor = circle_ctor.clone();
958                                    if circle.start == segment_id {
959                                        circle_ctor.start = ctor.position;
960                                    } else {
961                                        circle_ctor.center = ctor.position;
962                                    }
963                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
964                                } else {
965                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
966                                        "Internal: Circle does not have circle ctor, but found {}",
967                                        circle.ctor.human_friendly_kind_with_article()
968                                    ))));
969                                }
970                                continue;
971                            }
972                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
973                                let Some(control_index) =
974                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
975                                else {
976                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
977                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
978                                    ))));
979                                };
980                                if let Some(existing) = final_edits.get_mut(&owner_id) {
981                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
982                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
983                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
984                                            existing.human_friendly_kind_with_article()
985                                        ))));
986                                    };
987                                    spline_ctor.points[control_index] = ctor.position;
988                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
989                                    let mut spline_ctor = spline_ctor.clone();
990                                    spline_ctor.points[control_index] = ctor.position;
991                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
992                                } else {
993                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
994                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
995                                        spline.ctor.human_friendly_kind_with_article()
996                                    ))));
997                                }
998                                continue;
999                            }
1000                            _ => {}
1001                        }
1002                    }
1003
1004                    // No owner, it's an individual point
1005                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1006                }
1007                SegmentCtor::Line(ctor) => {
1008                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1009                }
1010                SegmentCtor::Arc(ctor) => {
1011                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1012                }
1013                SegmentCtor::Circle(ctor) => {
1014                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1015                }
1016                SegmentCtor::ControlPointSpline(ctor) => {
1017                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1018                }
1019            }
1020        }
1021
1022        for (segment_id, ctor) in final_edits {
1023            match ctor {
1024                SegmentCtor::Point(ctor) => self
1025                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1026                    .map_err(KclErrorWithOutputs::no_outputs)?,
1027                SegmentCtor::Line(ctor) => self
1028                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1029                    .map_err(KclErrorWithOutputs::no_outputs)?,
1030                SegmentCtor::Arc(ctor) => self
1031                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1032                    .map_err(KclErrorWithOutputs::no_outputs)?,
1033                SegmentCtor::Circle(ctor) => self
1034                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1035                    .map_err(KclErrorWithOutputs::no_outputs)?,
1036                SegmentCtor::ControlPointSpline(ctor) => self
1037                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1038                    .map_err(KclErrorWithOutputs::no_outputs)?,
1039            }
1040        }
1041        let (source_delta, mut scene_graph_delta) = self
1042            .execute_after_edit(
1043                ctx,
1044                sketch,
1045                sketch_block_ref,
1046                &mut new_ast,
1047                ExecuteAfterEditOptions {
1048                    segment_ids_edited: drag_anchor_segment_ids,
1049                    edit_kind: EditDeleteKind::Edit,
1050                    commit_solved_initial_guesses,
1051                },
1052            )
1053            .await?;
1054        if invalidates_ids {
1055            scene_graph_delta.invalidates_ids = true;
1056        }
1057        Ok((source_delta, scene_graph_delta))
1058    }
1059
1060    async fn delete_objects(
1061        &mut self,
1062        ctx: &ExecutorContext,
1063        _version: Version,
1064        sketch: ObjectId,
1065        constraint_ids: Vec<ObjectId>,
1066        segment_ids: Vec<ObjectId>,
1067    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1068        // TODO: Check version.
1069        let sketch_block_ref =
1070            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1071
1072        // Deduplicate IDs.
1073        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1074        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1075
1076        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1077        // also delete the point, as well as other points that are owned by the owner.
1078        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1079
1080        for segment_id in segment_ids_set.iter().copied() {
1081            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1082                let ObjectKind::Segment { segment } = &segment_object.kind else {
1083                    return None;
1084                };
1085                match segment {
1086                    Segment::Point(point) => point.owner,
1087                    Segment::Line(line) => line.owner,
1088                    _ => None,
1089                }
1090            });
1091
1092            if let Some(owner_id) = owner_id
1093                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1094                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1095                && matches!(
1096                    owner_segment,
1097                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1098                )
1099            {
1100                // segment is owned -> delete the owner
1101                resolved_segment_ids_to_delete.insert(owner_id);
1102            } else {
1103                // segment is not owned by anything -> can be deleted
1104                resolved_segment_ids_to_delete.insert(segment_id);
1105            }
1106        }
1107        let referenced_constraint_ids = self
1108            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1109            .map_err(KclErrorWithOutputs::no_outputs)?;
1110
1111        let mut new_ast = self.program.ast.clone();
1112
1113        for constraint_id in referenced_constraint_ids {
1114            if constraint_ids_set.contains(&constraint_id) {
1115                continue;
1116            }
1117
1118            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1119                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1120            })?;
1121            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1122                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1123                    "Object is not a constraint, it is {}",
1124                    constraint_object.kind.human_friendly_kind_with_article()
1125                ))));
1126            };
1127
1128            match constraint {
1129                Constraint::Coincident(coincident) => {
1130                    let remaining_segments =
1131                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1132
1133                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1134                    if remaining_segments.len() >= 2 {
1135                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1136                            .map_err(KclErrorWithOutputs::no_outputs)?;
1137                    } else {
1138                        constraint_ids_set.insert(constraint_id);
1139                    }
1140                }
1141                Constraint::EqualRadius(equal_radius) => {
1142                    let remaining_input = equal_radius
1143                        .input
1144                        .iter()
1145                        .copied()
1146                        .filter(|segment_id| {
1147                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1148                        })
1149                        .collect::<Vec<_>>();
1150
1151                    if remaining_input.len() >= 2 {
1152                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1153                            .map_err(KclErrorWithOutputs::no_outputs)?;
1154                    } else {
1155                        constraint_ids_set.insert(constraint_id);
1156                    }
1157                }
1158                Constraint::LinesEqualLength(lines_equal_length) => {
1159                    let remaining_lines = lines_equal_length
1160                        .lines
1161                        .iter()
1162                        .copied()
1163                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1164                        .collect::<Vec<_>>();
1165
1166                    // Equal length constraint is only valid with at least 2 lines
1167                    if remaining_lines.len() >= 2 {
1168                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1169                            .map_err(KclErrorWithOutputs::no_outputs)?;
1170                    } else {
1171                        constraint_ids_set.insert(constraint_id);
1172                    }
1173                }
1174                Constraint::Parallel(parallel) => {
1175                    let remaining_lines = parallel
1176                        .lines
1177                        .iter()
1178                        .copied()
1179                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1180                        .collect::<Vec<_>>();
1181
1182                    if remaining_lines.len() >= 2 {
1183                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1184                            .map_err(KclErrorWithOutputs::no_outputs)?;
1185                    } else {
1186                        constraint_ids_set.insert(constraint_id);
1187                    }
1188                }
1189                Constraint::Horizontal(Horizontal::Points { points }) => {
1190                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1191
1192                    if remaining_points.len() >= 2 {
1193                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1194                            .map_err(KclErrorWithOutputs::no_outputs)?;
1195                    } else {
1196                        constraint_ids_set.insert(constraint_id);
1197                    }
1198                }
1199                Constraint::Vertical(Vertical::Points { points }) => {
1200                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1201
1202                    if remaining_points.len() >= 2 {
1203                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1204                            .map_err(KclErrorWithOutputs::no_outputs)?;
1205                    } else {
1206                        constraint_ids_set.insert(constraint_id);
1207                    }
1208                }
1209                Constraint::Fixed(fixed) => {
1210                    if fixed.points.iter().any(|fixed_point| {
1211                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1212                    }) {
1213                        constraint_ids_set.insert(constraint_id);
1214                    }
1215                }
1216                _ => {
1217                    // All other constraint types: if referenced by a segment -> delete the constraint
1218                    constraint_ids_set.insert(constraint_id);
1219                }
1220            }
1221        }
1222
1223        for constraint_id in constraint_ids_set {
1224            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1225                .map_err(KclErrorWithOutputs::no_outputs)?;
1226        }
1227        for segment_id in resolved_segment_ids_to_delete {
1228            self.delete_segment(&mut new_ast, sketch, segment_id)
1229                .map_err(KclErrorWithOutputs::no_outputs)?;
1230        }
1231
1232        self.execute_after_edit(
1233            ctx,
1234            sketch,
1235            sketch_block_ref,
1236            &mut new_ast,
1237            ExecuteAfterEditOptions {
1238                segment_ids_edited: Default::default(),
1239                edit_kind: EditDeleteKind::DeleteNonSketch,
1240                commit_solved_initial_guesses: true,
1241            },
1242        )
1243        .await
1244    }
1245
1246    async fn add_constraint(
1247        &mut self,
1248        ctx: &ExecutorContext,
1249        _version: Version,
1250        sketch: ObjectId,
1251        constraint: Constraint,
1252    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1253        // TODO: Check version.
1254
1255        // Save the original state as a backup - we'll restore it if anything fails
1256        let original_program = self.program.clone();
1257        let original_scene_graph = self.scene_graph.clone();
1258
1259        let mut new_ast = self.program.ast.clone();
1260        let sketch_block_ref = match constraint {
1261            Constraint::Coincident(coincident) => self
1262                .add_coincident(sketch, coincident, &mut new_ast)
1263                .await
1264                .map_err(KclErrorWithOutputs::no_outputs)?,
1265            Constraint::Distance(distance) => self
1266                .add_distance(sketch, distance, &mut new_ast)
1267                .await
1268                .map_err(KclErrorWithOutputs::no_outputs)?,
1269            Constraint::EqualRadius(equal_radius) => self
1270                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1271                .await
1272                .map_err(KclErrorWithOutputs::no_outputs)?,
1273            Constraint::Fixed(fixed) => self
1274                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1275                .await
1276                .map_err(KclErrorWithOutputs::no_outputs)?,
1277            Constraint::HorizontalDistance(distance) => self
1278                .add_horizontal_distance(sketch, distance, &mut new_ast)
1279                .await
1280                .map_err(KclErrorWithOutputs::no_outputs)?,
1281            Constraint::VerticalDistance(distance) => self
1282                .add_vertical_distance(sketch, distance, &mut new_ast)
1283                .await
1284                .map_err(KclErrorWithOutputs::no_outputs)?,
1285            Constraint::Horizontal(horizontal) => self
1286                .add_horizontal(sketch, horizontal, &mut new_ast)
1287                .await
1288                .map_err(KclErrorWithOutputs::no_outputs)?,
1289            Constraint::LinesEqualLength(lines_equal_length) => self
1290                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1291                .await
1292                .map_err(KclErrorWithOutputs::no_outputs)?,
1293            Constraint::Midpoint(midpoint) => self
1294                .add_midpoint(sketch, midpoint, &mut new_ast)
1295                .await
1296                .map_err(KclErrorWithOutputs::no_outputs)?,
1297            Constraint::Parallel(parallel) => self
1298                .add_parallel(sketch, parallel, &mut new_ast)
1299                .await
1300                .map_err(KclErrorWithOutputs::no_outputs)?,
1301            Constraint::Perpendicular(perpendicular) => self
1302                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1303                .await
1304                .map_err(KclErrorWithOutputs::no_outputs)?,
1305            Constraint::Radius(radius) => self
1306                .add_radius(sketch, radius, &mut new_ast)
1307                .await
1308                .map_err(KclErrorWithOutputs::no_outputs)?,
1309            Constraint::Diameter(diameter) => self
1310                .add_diameter(sketch, diameter, &mut new_ast)
1311                .await
1312                .map_err(KclErrorWithOutputs::no_outputs)?,
1313            Constraint::Symmetric(symmetric) => self
1314                .add_symmetric(sketch, symmetric, &mut new_ast)
1315                .await
1316                .map_err(KclErrorWithOutputs::no_outputs)?,
1317            Constraint::Vertical(vertical) => self
1318                .add_vertical(sketch, vertical, &mut new_ast)
1319                .await
1320                .map_err(KclErrorWithOutputs::no_outputs)?,
1321            Constraint::Angle(lines_at_angle) => self
1322                .add_angle(sketch, lines_at_angle, &mut new_ast)
1323                .await
1324                .map_err(KclErrorWithOutputs::no_outputs)?,
1325            Constraint::Tangent(tangent) => self
1326                .add_tangent(sketch, tangent, &mut new_ast)
1327                .await
1328                .map_err(KclErrorWithOutputs::no_outputs)?,
1329        };
1330
1331        let result = self
1332            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1333            .await;
1334
1335        // If execution failed, restore the original state to prevent corruption
1336        if result.is_err() {
1337            self.program = original_program;
1338            self.scene_graph = original_scene_graph;
1339        }
1340
1341        result
1342    }
1343
1344    async fn chain_segment(
1345        &mut self,
1346        ctx: &ExecutorContext,
1347        version: Version,
1348        sketch: ObjectId,
1349        previous_segment_end_point_id: ObjectId,
1350        segment: SegmentCtor,
1351        _label: Option<String>,
1352    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1353        // TODO: Check version.
1354
1355        // First, add the segment (line) to get its start point ID
1356        let SegmentCtor::Line(line_ctor) = segment else {
1357            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1358                "chain_segment currently only supports Line segments, got {}",
1359                segment.human_friendly_kind_with_article(),
1360            ))));
1361        };
1362
1363        // Add the line segment first - this updates self.program and self.scene_graph
1364        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1365
1366        // Find the new line's start point ID from the updated scene graph
1367        // add_line updates self.scene_graph, so we can use that
1368        let new_line_id = first_scene_delta
1369            .new_objects
1370            .iter()
1371            .find(|&obj_id| {
1372                let obj = self.scene_graph.objects.get(obj_id.0);
1373                if let Some(obj) = obj {
1374                    matches!(
1375                        &obj.kind,
1376                        ObjectKind::Segment {
1377                            segment: Segment::Line(_)
1378                        }
1379                    )
1380                } else {
1381                    false
1382                }
1383            })
1384            .ok_or_else(|| {
1385                KclErrorWithOutputs::no_outputs(KclError::refactor(
1386                    "Failed to find new line segment in scene graph".to_string(),
1387                ))
1388            })?;
1389
1390        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1391            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1392                "New line object not found: {new_line_id:?}"
1393            )))
1394        })?;
1395
1396        let ObjectKind::Segment {
1397            segment: new_line_segment,
1398        } = &new_line_obj.kind
1399        else {
1400            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1401                "Object is not a segment: {new_line_obj:?}"
1402            ))));
1403        };
1404
1405        let Segment::Line(new_line) = new_line_segment else {
1406            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1407                "Segment is not a line: {new_line_segment:?}"
1408            ))));
1409        };
1410
1411        let new_line_start_point_id = new_line.start;
1412
1413        // Now add the coincident constraint between the previous end point and the new line's start point.
1414        let coincident = Coincident {
1415            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1416        };
1417
1418        let (final_src_delta, final_scene_delta) = self
1419            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1420            .await?;
1421
1422        // Combine new objects from the line addition and the constraint addition.
1423        // Both add_line and add_constraint now populate new_objects correctly.
1424        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1425        combined_new_objects.extend(final_scene_delta.new_objects);
1426
1427        let scene_graph_delta = SceneGraphDelta {
1428            new_graph: self.scene_graph_for_ui(),
1429            invalidates_ids: false,
1430            new_objects: combined_new_objects,
1431            exec_outcome: final_scene_delta.exec_outcome,
1432        };
1433
1434        Ok((final_src_delta, scene_graph_delta))
1435    }
1436
1437    async fn edit_constraint(
1438        &mut self,
1439        ctx: &ExecutorContext,
1440        _version: Version,
1441        sketch: ObjectId,
1442        constraint_id: ObjectId,
1443        value_expression: String,
1444    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1445        // TODO: Check version.
1446        let sketch_block_ref =
1447            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1448
1449        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1450            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1451        })?;
1452        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1453            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1454                "Object is not a constraint: {constraint_id:?}"
1455            ))));
1456        }
1457
1458        let mut new_ast = self.program.ast.clone();
1459
1460        // Parse the expression string into an AST node.
1461        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1462            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1463                "Invalid constraint value",
1464                &e,
1465            )))
1466        })?;
1467        if !errors.is_empty() {
1468            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1469                format_compilation_issues("Invalid constraint value", &errors),
1470            )));
1471        }
1472        let mut parsed = parsed.ok_or_else(|| {
1473            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1474        })?;
1475        if parsed.ast.body.is_empty() {
1476            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1477                "Empty value expression".to_string(),
1478            )));
1479        }
1480        let first = parsed.ast.body.remove(0);
1481        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1482            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1483                "Value expression must be a simple expression".to_string(),
1484            )));
1485        };
1486
1487        let new_value: ast::BinaryPart = expr_stmt
1488            .inner
1489            .expression
1490            .try_into()
1491            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1492
1493        self.mutate_ast(
1494            &mut new_ast,
1495            constraint_id,
1496            AstMutateCommand::EditConstraintValue { value: new_value },
1497        )
1498        .map_err(KclErrorWithOutputs::no_outputs)?;
1499
1500        self.execute_after_edit(
1501            ctx,
1502            sketch,
1503            sketch_block_ref,
1504            &mut new_ast,
1505            ExecuteAfterEditOptions {
1506                segment_ids_edited: Default::default(),
1507                edit_kind: EditDeleteKind::Edit,
1508                commit_solved_initial_guesses: true,
1509            },
1510        )
1511        .await
1512    }
1513
1514    async fn edit_distance_constraint_label_position(
1515        &mut self,
1516        ctx: &ExecutorContext,
1517        _version: Version,
1518        sketch: ObjectId,
1519        constraint_id: ObjectId,
1520        label_position: Point2d<Number>,
1521        anchor_segment_ids: Vec<ObjectId>,
1522    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1523        // TODO: Check version.
1524        let sketch_block_ref =
1525            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1528            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1529        })?;
1530        if !matches!(
1531            &object.kind,
1532            ObjectKind::Constraint {
1533                constraint: Constraint::Distance(_)
1534                    | Constraint::HorizontalDistance(_)
1535                    | Constraint::VerticalDistance(_)
1536                    | Constraint::Radius(_)
1537                    | Constraint::Diameter(_),
1538            }
1539        ) {
1540            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1541                "Object does not support labelPosition: {constraint_id:?}"
1542            ))));
1543        }
1544
1545        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1546            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1547                "Could not convert label position to AST: {err}"
1548            )))
1549        })?;
1550        let mut new_ast = self.program.ast.clone();
1551        self.mutate_ast(
1552            &mut new_ast,
1553            constraint_id,
1554            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1555        )
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        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4853
4854        if freedom_analysis_ran {
4855            // When freedom analysis ran, replace the cache entirely with new values
4856            // Don't merge with old values since IDs might have changed
4857            self.point_freedom_cache.clear();
4858            for new_obj in &new_objects {
4859                if let ObjectKind::Segment {
4860                    segment: crate::front::Segment::Point(point),
4861                } = &new_obj.kind
4862                {
4863                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4864                }
4865            }
4866            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4867            // Objects are already correct from the analysis, just use them as-is
4868            self.scene_graph.objects = new_objects;
4869        } else {
4870            // When freedom analysis didn't run, preserve old values and merge
4871            // Before replacing objects, extract and store freedom values from old objects
4872            for old_obj in &self.scene_graph.objects {
4873                if let ObjectKind::Segment {
4874                    segment: crate::front::Segment::Point(point),
4875                } = &old_obj.kind
4876                {
4877                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4878                }
4879            }
4880
4881            // Update objects, preserving stored freedom values when new is Free (might be default)
4882            let mut updated_objects = Vec::with_capacity(new_objects.len());
4883            for new_obj in new_objects {
4884                let mut obj = new_obj;
4885                if let ObjectKind::Segment {
4886                    segment: crate::front::Segment::Point(point),
4887                } = &mut obj.kind
4888                {
4889                    let new_freedom = point.freedom;
4890                    // When freedom_analysis=false, new values are defaults (Free).
4891                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4892                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4893                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4894                    // when there are actually unsatisfied constraints.
4895                    match new_freedom {
4896                        Freedom::Free => {
4897                            match self.point_freedom_cache.get(&obj.id).copied() {
4898                                Some(Freedom::Conflict) => {
4899                                    // Don't preserve Conflict - conflicts are transient
4900                                    // Keep it as Free
4901                                }
4902                                Some(Freedom::Fixed) => {
4903                                    // Preserve Fixed cached value
4904                                    point.freedom = Freedom::Fixed;
4905                                }
4906                                Some(Freedom::Free) => {
4907                                    // If stored is also Free, keep Free (no change needed)
4908                                }
4909                                None => {
4910                                    // If no cached value, keep Free (default)
4911                                }
4912                            }
4913                        }
4914                        Freedom::Fixed => {
4915                            // Use new value (already set)
4916                        }
4917                        Freedom::Conflict => {
4918                            // Use new value (already set)
4919                        }
4920                    }
4921                    // Store the new freedom value (even if it's Free, so we know it was set)
4922                    self.point_freedom_cache.insert(obj.id, point.freedom);
4923                }
4924                updated_objects.push(obj);
4925            }
4926
4927            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4928            self.scene_graph.objects = updated_objects;
4929        }
4930        outcome
4931    }
4932
4933    fn mutate_ast(
4934        &mut self,
4935        ast: &mut ast::Node<ast::Program>,
4936        object_id: ObjectId,
4937        command: AstMutateCommand,
4938    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4939        let sketch_object = self
4940            .scene_graph
4941            .objects
4942            .get(object_id.0)
4943            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4944        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4945    }
4946}
4947
4948fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4949    // Look up existing sketch.
4950    let sketch_object = scene_graph
4951        .objects
4952        .get(sketch_id.0)
4953        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4954    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4955        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4956    };
4957    expect_single_node_ref(sketch_object)
4958}
4959
4960fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4961    match &object.source {
4962        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4963            range: *range,
4964            node_path: node_path.clone(),
4965        }),
4966        SourceRef::BackTrace { ranges } => {
4967            let [range] = ranges.as_slice() else {
4968                return Err(KclError::refactor(format!(
4969                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4970                    ranges.len()
4971                )));
4972            };
4973            Ok(AstNodeRef {
4974                range: range.0,
4975                node_path: range.1.clone(),
4976            })
4977        }
4978    }
4979}
4980
4981/// This is a deprecated fall-back implementation. Prefer
4982/// [`only_sketch_block()`] to avoid reliance on source ranges.
4983fn only_sketch_block_from_range(
4984    ast: &mut ast::Node<ast::Program>,
4985    sketch_block_range: SourceRange,
4986    edit_kind: ChangeKind,
4987) -> Result<(), KclError> {
4988    let r1 = sketch_block_range;
4989    let matches_range = |r2: SourceRange| -> bool {
4990        // We may have added items to the sketch block, so the end may not be an
4991        // exact match.
4992        match edit_kind {
4993            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4994            // For edit, we don't know whether it grew or shrank.
4995            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4996            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4997            // No edit should be an exact match.
4998            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4999        }
5000    };
5001    let mut found = false;
5002    for item in ast.body.iter_mut() {
5003        match item {
5004            ast::BodyItem::ImportStatement(_) => {}
5005            ast::BodyItem::ExpressionStatement(node) => {
5006                if matches_range(SourceRange::from(&*node))
5007                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5008                {
5009                    sketch_block.is_being_edited = true;
5010                    found = true;
5011                    break;
5012                }
5013            }
5014            ast::BodyItem::VariableDeclaration(node) => {
5015                if matches_range(SourceRange::from(&node.declaration.init))
5016                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5017                {
5018                    sketch_block.is_being_edited = true;
5019                    found = true;
5020                    break;
5021                }
5022            }
5023            ast::BodyItem::TypeDeclaration(_) => {}
5024            ast::BodyItem::ReturnStatement(node) => {
5025                if matches_range(SourceRange::from(&node.argument))
5026                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5027                {
5028                    sketch_block.is_being_edited = true;
5029                    found = true;
5030                    break;
5031                }
5032            }
5033        }
5034    }
5035    if !found {
5036        return Err(KclError::refactor(format!(
5037            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5038        )));
5039    }
5040
5041    Ok(())
5042}
5043
5044fn only_sketch_block(
5045    ast: &mut ast::Node<ast::Program>,
5046    sketch_block_ref: &AstNodeRef,
5047    edit_kind: ChangeKind,
5048) -> Result<(), KclError> {
5049    let Some(target_node_path) = &sketch_block_ref.node_path else {
5050        #[cfg(target_arch = "wasm32")]
5051        web_sys::console::warn_1(
5052            &format!(
5053                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5054                sketch_block_ref
5055            )
5056            .into(),
5057        );
5058        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5059    };
5060    let mut found = false;
5061    for item in ast.body.iter_mut() {
5062        match item {
5063            ast::BodyItem::ImportStatement(_) => {}
5064            ast::BodyItem::ExpressionStatement(node) => {
5065                // Check the statement.
5066                if let Some(node_path) = &node.node_path
5067                    && node_path == target_node_path
5068                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5069                {
5070                    sketch_block.is_being_edited = true;
5071                    found = true;
5072                    break;
5073                }
5074                // Check the expression.
5075                if let Some(node_path) = node.expression.node_path()
5076                    && node_path == target_node_path
5077                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5078                {
5079                    sketch_block.is_being_edited = true;
5080                    found = true;
5081                    break;
5082                }
5083            }
5084            ast::BodyItem::VariableDeclaration(node) => {
5085                if let Some(node_path) = node.declaration.init.node_path()
5086                    && node_path == target_node_path
5087                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5088                {
5089                    sketch_block.is_being_edited = true;
5090                    found = true;
5091                    break;
5092                }
5093            }
5094            ast::BodyItem::TypeDeclaration(_) => {}
5095            ast::BodyItem::ReturnStatement(node) => {
5096                if let Some(node_path) = node.argument.node_path()
5097                    && node_path == target_node_path
5098                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5099                {
5100                    sketch_block.is_being_edited = true;
5101                    found = true;
5102                    break;
5103                }
5104            }
5105        }
5106    }
5107    if !found {
5108        return Err(KclError::refactor(format!(
5109            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5110        )));
5111    }
5112
5113    Ok(())
5114}
5115
5116fn sketch_on_ast_expr(
5117    ast: &mut ast::Node<ast::Program>,
5118    scene_graph: &SceneGraph,
5119    on: &Plane,
5120) -> Result<ast::Expr, KclError> {
5121    match on {
5122        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5123        Plane::Object(object_id) => {
5124            let on_object = scene_graph
5125                .objects
5126                .get(object_id.0)
5127                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5128            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5129                return Ok(face_expr);
5130            }
5131            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5132        }
5133    }
5134}
5135
5136fn sketch_face_of_scene_object_ast_expr(
5137    ast: &mut ast::Node<ast::Program>,
5138    on_object: &crate::front::Object,
5139) -> Result<Option<ast::Expr>, KclError> {
5140    match &on_object.kind {
5141        ObjectKind::Wall(wall) => {
5142            let solid_ref = get_or_insert_ast_reference(
5143                ast,
5144                &source_ref_from_source_ref_range(&wall.source.solid),
5145                "solid",
5146                None,
5147            )?;
5148            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5149                return Err(KclError::refactor(format!(
5150                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5151                    on_object.artifact_id
5152                )));
5153            };
5154            let solid_expr = indexed_solid_expr_for_sweep_output(
5155                ast_name_expr(solid_name_expr.name.name.clone()),
5156                wall.solid_output_index,
5157            );
5158            let sweep_ref = get_or_insert_ast_reference(
5159                ast,
5160                &source_ref_from_source_ref_range(&wall.source.sweep),
5161                "solid",
5162                None,
5163            )?;
5164            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5165                return Err(KclError::refactor(format!(
5166                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5167                    on_object.artifact_id
5168                )));
5169            };
5170            let sweep_name = sweep_name_expr.name.name.clone();
5171            let segment_ref = get_or_insert_ast_reference(
5172                ast,
5173                &source_ref_from_source_ref_range(&wall.source.segment),
5174                LINE_VARIABLE,
5175                None,
5176            )?;
5177
5178            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5179                wall.source
5180                    .path
5181                    .as_ref()
5182                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5183            }) {
5184                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5185                    return Err(KclError::refactor(format!(
5186                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5187                        on_object.artifact_id
5188                    )));
5189                };
5190                create_member_expression(
5191                    create_member_expression(ast_name_expr(region_name), "tags"),
5192                    &segment_name_expr.name.name,
5193                )
5194            } else {
5195                segment_ref
5196            };
5197
5198            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5199        }
5200        ObjectKind::Cap(cap) => {
5201            let solid_ref =
5202                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5203            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5204                return Err(KclError::refactor(format!(
5205                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5206                    on_object.artifact_id
5207                )));
5208            };
5209            let solid_expr = indexed_solid_expr_for_sweep_output(
5210                ast_name_expr(solid_name_expr.name.name.clone()),
5211                cap.solid_output_index,
5212            );
5213            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5214            let face_expr = match cap.kind {
5215                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5216                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5217            };
5218
5219            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5220        }
5221        _ => Ok(None),
5222    }
5223}
5224
5225fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5226    match solid_output_index {
5227        Some(output_index) => create_index_expression(solid_expr, output_index),
5228        None => solid_expr,
5229    }
5230}
5231
5232fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5233    SourceRef::Simple {
5234        range: source.range,
5235        node_path: source.node_path.clone(),
5236    }
5237}
5238
5239fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5240    let source_ref = source_ref_from_source_ref_range(path_source);
5241    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5242    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5243        return None;
5244    };
5245    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5246        return None;
5247    };
5248    if region_call.callee.name.name != "region" {
5249        return None;
5250    }
5251    Some(candidate)
5252}
5253
5254fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5255    let mut current_id = source_id;
5256    let mut current_composite = None;
5257    let mut visited = HashSet::new();
5258
5259    while visited.insert(current_id) {
5260        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5261
5262        let Some(composite_id) = next_composite_id else {
5263            break;
5264        };
5265        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5266            break;
5267        };
5268
5269        current_id = composite.id;
5270        current_composite = Some(composite);
5271
5272        if !composite.consumed {
5273            break;
5274        }
5275    }
5276
5277    current_composite.map(|composite| &composite.code_ref)
5278}
5279
5280fn downstream_composite_id_for_solid_source(
5281    artifact_graph: &ArtifactGraph,
5282    source_id: ArtifactId,
5283) -> Option<ArtifactId> {
5284    // Source is a path, find its solid.
5285    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5286        && let Some(composite_id) = path.composite_solid_id
5287        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5288        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5289    {
5290        return Some(composite_id);
5291    }
5292
5293    // Source is a sweep, find its path -> then find the solid
5294    for artifact in artifact_graph.values() {
5295        if let Artifact::Path(path) = artifact
5296            && path.sweep_id == Some(source_id)
5297            && let Some(composite_id) = path.composite_solid_id
5298            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5299            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5300        {
5301            return Some(composite_id);
5302        }
5303    }
5304
5305    // Source is a solid, find its downstream solid.
5306    artifact_graph.values().find_map(|artifact| {
5307        let Artifact::CompositeSolid(composite) = artifact else {
5308            return None;
5309        };
5310        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5311    })
5312}
5313
5314fn composite_contains_path_input(
5315    solid_ids: &[ArtifactId],
5316    tool_ids: &[ArtifactId],
5317    path_id: ArtifactId,
5318    solid2d_id: Option<ArtifactId>,
5319) -> bool {
5320    composite_contains_input(solid_ids, tool_ids, path_id)
5321        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5322}
5323
5324fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5325    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5326}
5327
5328fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5329    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5330    SourceRefRange {
5331        range: code_ref.range,
5332        node_path,
5333    }
5334}
5335
5336fn solid_output_index_for_sweep(
5337    artifact_graph: &ArtifactGraph,
5338    sweep_id: ArtifactId,
5339    sweep_code_ref: &CodeRef,
5340) -> Option<usize> {
5341    let sibling_sweeps = artifact_graph
5342        .values()
5343        .filter_map(|artifact| match artifact {
5344            Artifact::Sweep(sweep)
5345                if sweep.code_ref.range == sweep_code_ref.range
5346                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5347            {
5348                Some(sweep)
5349            }
5350            _ => None,
5351        })
5352        .collect::<Vec<_>>();
5353
5354    if sibling_sweeps.len() <= 1 {
5355        return None;
5356    }
5357
5358    sibling_sweeps
5359        .iter()
5360        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5361}
5362
5363fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5364    let mut existing_artifact_ids = scene_objects
5365        .iter()
5366        .map(|object| object.artifact_id)
5367        .collect::<HashSet<_>>();
5368
5369    for artifact in artifact_graph.values() {
5370        match artifact {
5371            Artifact::Wall(wall) => {
5372                if existing_artifact_ids.contains(&wall.id) {
5373                    continue;
5374                }
5375
5376                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5377                    Artifact::Segment(segment) => Some(segment),
5378                    _ => None,
5379                }) else {
5380                    continue;
5381                };
5382                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5383                    Artifact::Sweep(sweep) => Some(sweep),
5384                    _ => None,
5385                }) else {
5386                    continue;
5387                };
5388                let source_segment = segment
5389                    .original_seg_id
5390                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5391                    .and_then(|artifact| match artifact {
5392                        Artifact::Segment(segment) => Some(segment),
5393                        _ => None,
5394                    })
5395                    .unwrap_or(segment);
5396                let solid_code_ref =
5397                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5398                let path_code_ref = artifact_graph
5399                    .get(&segment.path_id)
5400                    .or_else(|| artifact_graph.get(&sweep.path_id))
5401                    .and_then(|artifact| match artifact {
5402                        Artifact::Path(path) => Some(&path.code_ref),
5403                        _ => None,
5404                    });
5405                let source = WallSource {
5406                    solid: code_ref_source_ref_range(solid_code_ref),
5407                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5408                    path: path_code_ref.map(code_ref_source_ref_range),
5409                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5410                };
5411                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5412                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5413                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5414                    .flatten();
5415                let object_source = source_ref_from_source_ref_range(&source.solid);
5416                let id = ObjectId(scene_objects.len());
5417                scene_objects.push(crate::front::Object {
5418                    id,
5419                    kind: ObjectKind::Wall(crate::frontend::api::Wall {
5420                        id,
5421                        source,
5422                        solid_output_index,
5423                    }),
5424                    label: Default::default(),
5425                    comments: Default::default(),
5426                    artifact_id: wall.id,
5427                    source: object_source,
5428                });
5429                existing_artifact_ids.insert(wall.id);
5430            }
5431            Artifact::Cap(cap) => {
5432                if existing_artifact_ids.contains(&cap.id) {
5433                    continue;
5434                }
5435
5436                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5437                    Artifact::Sweep(sweep) => Some(sweep),
5438                    _ => None,
5439                }) else {
5440                    continue;
5441                };
5442                let id = ObjectId(scene_objects.len());
5443                let kind = match cap.sub_type {
5444                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5445                    CapSubType::End => crate::frontend::api::CapKind::End,
5446                };
5447                let solid_code_ref =
5448                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5449                let source = CapSource {
5450                    solid: code_ref_source_ref_range(solid_code_ref),
5451                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5452                };
5453                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5454                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5455                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5456                    .flatten();
5457                let object_source = source_ref_from_source_ref_range(&source.solid);
5458                scene_objects.push(crate::front::Object {
5459                    id,
5460                    kind: ObjectKind::Cap(crate::frontend::api::Cap {
5461                        id,
5462                        kind,
5463                        source,
5464                        solid_output_index,
5465                    }),
5466                    label: Default::default(),
5467                    comments: Default::default(),
5468                    artifact_id: cap.id,
5469                    source: object_source,
5470                });
5471                existing_artifact_ids.insert(cap.id);
5472            }
5473            _ => {}
5474        }
5475    }
5476}
5477
5478fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5479    use crate::engine::PlaneName;
5480
5481    match name {
5482        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5483        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5484        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5485        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5486        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5487        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5488    }
5489}
5490
5491fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5492    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5493        ast::UnaryOperator::Neg,
5494        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5495    )))
5496}
5497
5498fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5499    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5500        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5501        unlabeled: Some(solid_expr),
5502        arguments: vec![ast::LabeledArg {
5503            label: Some(ast::Identifier::new("face")),
5504            arg: face_expr,
5505        }],
5506        digest: None,
5507        non_code_meta: Default::default(),
5508    })))
5509}
5510
5511fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5512    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5513        return None;
5514    };
5515    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5516        return None;
5517    };
5518    if !matches!(
5519        sweep_call.callee.name.name.as_str(),
5520        "extrude" | "revolve" | "sweep" | "loft"
5521    ) {
5522        return None;
5523    }
5524    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5525        return None;
5526    };
5527    let candidate = region_name_expr.name.name.clone();
5528    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5529        return None;
5530    };
5531    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5532        return None;
5533    };
5534    if region_call.callee.name.name != "region" {
5535        return None;
5536    }
5537    Some(candidate)
5538}
5539
5540/// Return the AST expression referencing the variable at the given source ref.
5541/// If no such variable exists, insert a new variable declaration with the given
5542/// prefix.
5543///
5544/// This may return a complex expression referencing properties of the variable
5545/// (e.g., `line1.start`).
5546fn get_or_insert_ast_reference(
5547    ast: &mut ast::Node<ast::Program>,
5548    source_ref: &SourceRef,
5549    prefix: &str,
5550    property: Option<&str>,
5551) -> Result<ast::Expr, KclError> {
5552    let command = AstMutateCommand::AddVariableDeclaration {
5553        prefix: prefix.to_owned(),
5554    };
5555    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5556        Ok((_, ret)) => ret,
5557        Err(err) => {
5558            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5559                AstMutateCommandReturn::Name(var_name)
5560            } else {
5561                return Err(err);
5562            }
5563        }
5564    };
5565    let AstMutateCommandReturn::Name(var_name) = ret else {
5566        return Err(KclError::refactor(
5567            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5568        ));
5569    };
5570    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5571    let Some(property) = property else {
5572        // No property; just return the variable name.
5573        return Ok(var_expr);
5574    };
5575
5576    Ok(create_member_expression(var_expr, property))
5577}
5578
5579fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5580    let source_range = match source_ref {
5581        SourceRef::Simple { range, .. } => *range,
5582        SourceRef::BackTrace { ranges } => {
5583            let [range] = ranges.as_slice() else {
5584                return None;
5585            };
5586            range.0
5587        }
5588    };
5589    ast.body.iter().find_map(|item| {
5590        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5591            return None;
5592        };
5593        let init_range = SourceRange::from(&var_decl.declaration.init);
5594        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5595            && init_range.start() <= source_range.start()
5596            && source_range.end() <= init_range.end();
5597        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5598            && init_range != source_range
5599            && source_is_inside_init
5600        {
5601            return None;
5602        }
5603        source_is_inside_init.then(|| var_decl.name().to_owned())
5604    })
5605}
5606
5607fn mutate_ast_node_by_source_ref(
5608    ast: &mut ast::Node<ast::Program>,
5609    source_ref: &SourceRef,
5610    command: AstMutateCommand,
5611) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5612    let (source_range, node_path) = match source_ref {
5613        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5614        SourceRef::BackTrace { ranges } => {
5615            let [range] = ranges.as_slice() else {
5616                return Err(KclError::refactor(format!(
5617                    "Expected single source ref, got {}; ranges={ranges:#?}",
5618                    ranges.len(),
5619                )));
5620            };
5621            (range.0, range.1.clone())
5622        }
5623    };
5624    let mut context = AstMutateContext {
5625        source_range,
5626        node_path,
5627        command,
5628        defined_names_stack: Default::default(),
5629    };
5630    let control = dfs_mut(ast, &mut context);
5631    match control {
5632        ControlFlow::Continue(_) => Err(KclError::refactor(
5633            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5634        )),
5635        ControlFlow::Break(break_value) => break_value,
5636    }
5637}
5638
5639#[derive(Debug)]
5640struct AstMutateContext {
5641    source_range: SourceRange,
5642    node_path: Option<ast::NodePath>,
5643    command: AstMutateCommand,
5644    defined_names_stack: Vec<HashSet<String>>,
5645}
5646
5647#[derive(Debug)]
5648#[allow(clippy::large_enum_variant)]
5649enum AstMutateCommand {
5650    /// Add an expression statement to the sketch block.
5651    AddSketchBlockExprStmt {
5652        expr: ast::Expr,
5653    },
5654    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5655    AddSketchBlockVarDecl {
5656        prefix: String,
5657        expr: ast::Expr,
5658    },
5659    AddVariableDeclaration {
5660        prefix: String,
5661    },
5662    EditPoint {
5663        at: ast::Expr,
5664    },
5665    EditLine {
5666        start: ast::Expr,
5667        end: ast::Expr,
5668        construction: Option<bool>,
5669    },
5670    EditArc {
5671        start: ast::Expr,
5672        end: ast::Expr,
5673        center: ast::Expr,
5674        construction: Option<bool>,
5675    },
5676    EditCircle {
5677        start: ast::Expr,
5678        center: ast::Expr,
5679        construction: Option<bool>,
5680    },
5681    EditControlPointSpline {
5682        points: ast::Expr,
5683        construction: Option<bool>,
5684    },
5685    EditConstraintValue {
5686        value: ast::BinaryPart,
5687    },
5688    EditDistanceConstraintLabelPosition {
5689        label_position: ast::Expr,
5690    },
5691    EditCallUnlabeled {
5692        arg: ast::Expr,
5693    },
5694    EditVarInitialValue {
5695        value: Number,
5696    },
5697    DeleteNode,
5698}
5699
5700impl AstMutateCommand {
5701    fn needs_defined_names_stack(&self) -> bool {
5702        matches!(
5703            self,
5704            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5705        )
5706    }
5707}
5708
5709#[derive(Debug)]
5710enum AstMutateCommandReturn {
5711    None,
5712    Name(String),
5713}
5714
5715#[derive(Debug, Clone)]
5716struct AstNodeRef {
5717    range: SourceRange,
5718    node_path: Option<ast::NodePath>,
5719}
5720
5721impl<T> From<&ast::Node<T>> for AstNodeRef {
5722    fn from(value: &ast::Node<T>) -> Self {
5723        AstNodeRef {
5724            range: value.into(),
5725            node_path: value.node_path.clone(),
5726        }
5727    }
5728}
5729
5730impl From<&ast::BodyItem> for AstNodeRef {
5731    fn from(value: &ast::BodyItem) -> Self {
5732        match value {
5733            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5734                range: node.into(),
5735                node_path: node.node_path.clone(),
5736            },
5737            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5738                range: node.into(),
5739                node_path: node.node_path.clone(),
5740            },
5741            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5742                range: node.into(),
5743                node_path: node.node_path.clone(),
5744            },
5745            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5746                range: node.into(),
5747                node_path: node.node_path.clone(),
5748            },
5749            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5750                range: node.into(),
5751                node_path: node.node_path.clone(),
5752            },
5753        }
5754    }
5755}
5756
5757impl From<&ast::Expr> for AstNodeRef {
5758    fn from(value: &ast::Expr) -> Self {
5759        AstNodeRef {
5760            range: SourceRange::from(value),
5761            node_path: value.node_path().cloned(),
5762        }
5763    }
5764}
5765
5766impl From<&AstMutateContext> for AstNodeRef {
5767    fn from(value: &AstMutateContext) -> Self {
5768        AstNodeRef {
5769            range: value.source_range,
5770            node_path: value.node_path.clone(),
5771        }
5772    }
5773}
5774
5775impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5776    type Error = crate::walk::AstNodeError;
5777
5778    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5779        Ok(AstNodeRef {
5780            range: SourceRange::try_from(value)?,
5781            node_path: value.try_into()?,
5782        })
5783    }
5784}
5785
5786impl From<AstNodeRef> for SourceRange {
5787    fn from(value: AstNodeRef) -> Self {
5788        value.range
5789    }
5790}
5791
5792impl Visitor for AstMutateContext {
5793    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5794    type Continue = ();
5795
5796    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5797        filter_and_process(self, node)
5798    }
5799
5800    fn finish(&mut self, node: NodeMut<'_>) {
5801        match &node {
5802            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5803                self.defined_names_stack.pop();
5804            }
5805            _ => {}
5806        }
5807    }
5808}
5809
5810fn filter_and_process(
5811    ctx: &mut AstMutateContext,
5812    node: NodeMut,
5813) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5814    let Ok(node_range) = SourceRange::try_from(&node) else {
5815        // Nodes that can't be converted to a range aren't interesting.
5816        return TraversalReturn::new_continue(());
5817    };
5818    // If we're adding a variable declaration, we need to look at variable
5819    // declaration expressions to see if it already has a variable, before
5820    // continuing. The variable declaration's source range won't match the
5821    // target; its init expression will.
5822    if let NodeMut::VariableDeclaration(var_decl) = &node {
5823        let expr_range = SourceRange::from(&var_decl.declaration.init);
5824        let expr_node_path = var_decl.declaration.init.node_path();
5825        if source_ref_matches(ctx, expr_range, expr_node_path) {
5826            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5827                // We found the variable declaration expression. It doesn't need
5828                // to be added.
5829                return TraversalReturn::new_break(Ok((
5830                    AstNodeRef::from(&**var_decl),
5831                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5832                )));
5833            }
5834            if let AstMutateCommand::DeleteNode = &ctx.command {
5835                // We found the variable declaration. Delete the variable along
5836                // with the segment.
5837                return TraversalReturn {
5838                    mutate_body_item: MutateBodyItem::Delete,
5839                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5840                };
5841            }
5842        }
5843    }
5844    // Similar thing with expression statement. We need to look at the
5845    // expression inside it.
5846    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5847        let expr_range = SourceRange::from(&expr_stmt.expression);
5848        let expr_node_path = expr_stmt.expression.node_path();
5849        if source_ref_matches(ctx, expr_range, expr_node_path) {
5850            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5851                // We found the node wrapped in an expression statement. Process
5852                // the statement.
5853                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5854                    return TraversalReturn::new_continue(());
5855                };
5856                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5857            }
5858            if let AstMutateCommand::DeleteNode = &ctx.command {
5859                // We found the node wrapped in an expression statement. Delete
5860                // the whole statement.
5861                return TraversalReturn {
5862                    mutate_body_item: MutateBodyItem::Delete,
5863                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5864                };
5865            }
5866        }
5867    }
5868
5869    if ctx.command.needs_defined_names_stack() {
5870        if let NodeMut::Program(program) = &node {
5871            ctx.defined_names_stack.push(find_defined_names(*program));
5872        } else if let NodeMut::SketchBlock(block) = &node {
5873            ctx.defined_names_stack.push(find_defined_names(&block.body));
5874        }
5875    }
5876
5877    // Make sure the node matches the source ref.
5878    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5879    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5880        return TraversalReturn::new_continue(());
5881    }
5882    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5883        return TraversalReturn::new_continue(());
5884    };
5885    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5886}
5887
5888fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5889    match &ctx.node_path {
5890        Some(target) => Some(target) == node_path,
5891        None => node_range == ctx.source_range,
5892    }
5893}
5894
5895fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5896    match &ctx.command {
5897        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5898            if let NodeMut::SketchBlock(sketch_block) = node {
5899                sketch_block
5900                    .body
5901                    .items
5902                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5903                        inner: ast::ExpressionStatement {
5904                            expression: expr.clone(),
5905                            digest: None,
5906                        },
5907                        start: Default::default(),
5908                        end: Default::default(),
5909                        module_id: Default::default(),
5910                        node_path: None,
5911                        outer_attrs: Default::default(),
5912                        pre_comments: Default::default(),
5913                        comment_start: Default::default(),
5914                    }));
5915                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5916            }
5917        }
5918        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5919            if let NodeMut::SketchBlock(sketch_block) = node {
5920                let empty_defined_names = HashSet::new();
5921                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5922                let Ok(name) = next_free_name(prefix, defined_names) else {
5923                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5924                };
5925                sketch_block
5926                    .body
5927                    .items
5928                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5929                        ast::VariableDeclaration::new(
5930                            ast::VariableDeclarator::new(&name, expr.clone()),
5931                            ast::ItemVisibility::Default,
5932                            ast::VariableKind::Const,
5933                        ),
5934                    ))));
5935                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5936            }
5937        }
5938        AstMutateCommand::AddVariableDeclaration { prefix } => {
5939            if let NodeMut::VariableDeclaration(inner) = node {
5940                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5941            }
5942            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5943                let empty_defined_names = HashSet::new();
5944                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5945                let Ok(name) = next_free_name(prefix, defined_names) else {
5946                    // TODO: Return an error instead?
5947                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5948                };
5949                let mutate_node =
5950                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5951                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5952                        ast::ItemVisibility::Default,
5953                        ast::VariableKind::Const,
5954                    ))));
5955                return TraversalReturn {
5956                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5957                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5958                };
5959            }
5960        }
5961        AstMutateCommand::EditPoint { at } => {
5962            if let NodeMut::CallExpressionKw(call) = node {
5963                if call.callee.name.name != POINT_FN {
5964                    return TraversalReturn::new_continue(());
5965                }
5966                // Update the arguments.
5967                for labeled_arg in &mut call.arguments {
5968                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5969                        labeled_arg.arg = at.clone();
5970                    }
5971                }
5972                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5973            }
5974        }
5975        AstMutateCommand::EditLine {
5976            start,
5977            end,
5978            construction,
5979        } => {
5980            if let NodeMut::CallExpressionKw(call) = node {
5981                if call.callee.name.name != LINE_FN {
5982                    return TraversalReturn::new_continue(());
5983                }
5984                // Update the arguments.
5985                for labeled_arg in &mut call.arguments {
5986                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5987                        labeled_arg.arg = start.clone();
5988                    }
5989                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5990                        labeled_arg.arg = end.clone();
5991                    }
5992                }
5993                // Handle construction kwarg
5994                if let Some(construction_value) = construction {
5995                    let construction_exists = call
5996                        .arguments
5997                        .iter()
5998                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5999                    if *construction_value {
6000                        // Add or update construction=true
6001                        if construction_exists {
6002                            // Update existing construction kwarg
6003                            for labeled_arg in &mut call.arguments {
6004                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6005                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6006                                        value: ast::LiteralValue::Bool(true),
6007                                        raw: "true".to_string(),
6008                                        digest: None,
6009                                    })));
6010                                }
6011                            }
6012                        } else {
6013                            // Add new construction kwarg
6014                            call.arguments.push(ast::LabeledArg {
6015                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6016                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6017                                    value: ast::LiteralValue::Bool(true),
6018                                    raw: "true".to_string(),
6019                                    digest: None,
6020                                }))),
6021                            });
6022                        }
6023                    } else {
6024                        // Remove construction kwarg if it exists
6025                        call.arguments
6026                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6027                    }
6028                }
6029                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6030            }
6031        }
6032        AstMutateCommand::EditArc {
6033            start,
6034            end,
6035            center,
6036            construction,
6037        } => {
6038            if let NodeMut::CallExpressionKw(call) = node {
6039                if call.callee.name.name != ARC_FN {
6040                    return TraversalReturn::new_continue(());
6041                }
6042                // Update the arguments.
6043                for labeled_arg in &mut call.arguments {
6044                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6045                        labeled_arg.arg = start.clone();
6046                    }
6047                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6048                        labeled_arg.arg = end.clone();
6049                    }
6050                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6051                        labeled_arg.arg = center.clone();
6052                    }
6053                }
6054                // Handle construction kwarg
6055                if let Some(construction_value) = construction {
6056                    let construction_exists = call
6057                        .arguments
6058                        .iter()
6059                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6060                    if *construction_value {
6061                        // Add or update construction=true
6062                        if construction_exists {
6063                            // Update existing construction kwarg
6064                            for labeled_arg in &mut call.arguments {
6065                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6066                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6067                                        value: ast::LiteralValue::Bool(true),
6068                                        raw: "true".to_string(),
6069                                        digest: None,
6070                                    })));
6071                                }
6072                            }
6073                        } else {
6074                            // Add new construction kwarg
6075                            call.arguments.push(ast::LabeledArg {
6076                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6077                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6078                                    value: ast::LiteralValue::Bool(true),
6079                                    raw: "true".to_string(),
6080                                    digest: None,
6081                                }))),
6082                            });
6083                        }
6084                    } else {
6085                        // Remove construction kwarg if it exists
6086                        call.arguments
6087                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6088                    }
6089                }
6090                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6091            }
6092        }
6093        AstMutateCommand::EditCircle {
6094            start,
6095            center,
6096            construction,
6097        } => {
6098            if let NodeMut::CallExpressionKw(call) = node {
6099                if call.callee.name.name != CIRCLE_FN {
6100                    return TraversalReturn::new_continue(());
6101                }
6102                // Update the arguments.
6103                for labeled_arg in &mut call.arguments {
6104                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6105                        labeled_arg.arg = start.clone();
6106                    }
6107                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6108                        labeled_arg.arg = center.clone();
6109                    }
6110                }
6111                // Handle construction kwarg
6112                if let Some(construction_value) = construction {
6113                    let construction_exists = call
6114                        .arguments
6115                        .iter()
6116                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6117                    if *construction_value {
6118                        if construction_exists {
6119                            // Update existing construction kwarg
6120                            for labeled_arg in &mut call.arguments {
6121                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6122                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6123                                        value: ast::LiteralValue::Bool(true),
6124                                        raw: "true".to_string(),
6125                                        digest: None,
6126                                    })));
6127                                }
6128                            }
6129                        } else {
6130                            // Add new construction kwarg
6131                            call.arguments.push(ast::LabeledArg {
6132                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6133                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6134                                    value: ast::LiteralValue::Bool(true),
6135                                    raw: "true".to_string(),
6136                                    digest: None,
6137                                }))),
6138                            });
6139                        }
6140                    } else {
6141                        // Remove construction kwarg if it exists
6142                        call.arguments
6143                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6144                    }
6145                }
6146                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6147            }
6148        }
6149        AstMutateCommand::EditControlPointSpline { points, construction } => {
6150            if let NodeMut::CallExpressionKw(call) = node {
6151                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6152                    return TraversalReturn::new_continue(());
6153                }
6154                for labeled_arg in &mut call.arguments {
6155                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6156                    {
6157                        labeled_arg.arg = points.clone();
6158                    }
6159                }
6160                // Handle construction kwarg
6161                if let Some(construction_value) = construction {
6162                    let construction_exists = call
6163                        .arguments
6164                        .iter()
6165                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6166                    if *construction_value {
6167                        if construction_exists {
6168                            for labeled_arg in &mut call.arguments {
6169                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6170                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6171                                        value: ast::LiteralValue::Bool(true),
6172                                        raw: "true".to_string(),
6173                                        digest: None,
6174                                    })));
6175                                }
6176                            }
6177                        } else {
6178                            call.arguments.push(ast::LabeledArg {
6179                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6180                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6181                                    value: ast::LiteralValue::Bool(true),
6182                                    raw: "true".to_string(),
6183                                    digest: None,
6184                                }))),
6185                            });
6186                        }
6187                    } else {
6188                        call.arguments
6189                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6190                    }
6191                }
6192                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6193            }
6194        }
6195        AstMutateCommand::EditConstraintValue { value } => {
6196            if let NodeMut::BinaryExpression(binary_expr) = node {
6197                let left_is_constraint = matches!(
6198                    &binary_expr.left,
6199                    ast::BinaryPart::CallExpressionKw(call)
6200                        if matches!(
6201                            call.callee.name.name.as_str(),
6202                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6203                        )
6204                );
6205                if left_is_constraint {
6206                    binary_expr.right = value.clone();
6207                } else {
6208                    binary_expr.left = value.clone();
6209                }
6210
6211                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6212            }
6213        }
6214        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6215            if let NodeMut::BinaryExpression(binary_expr) = node {
6216                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6217                    return TraversalReturn::new_continue(());
6218                };
6219                if !matches!(
6220                    call.callee.name.name.as_str(),
6221                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6222                ) {
6223                    return TraversalReturn::new_continue(());
6224                }
6225
6226                if let Some(label_arg) = call
6227                    .arguments
6228                    .iter_mut()
6229                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6230                {
6231                    label_arg.arg = label_position.clone();
6232                } else {
6233                    call.arguments.push(ast::LabeledArg {
6234                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6235                        arg: label_position.clone(),
6236                    });
6237                }
6238
6239                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6240            }
6241        }
6242        AstMutateCommand::EditCallUnlabeled { arg } => {
6243            if let NodeMut::CallExpressionKw(call) = node {
6244                call.unlabeled = Some(arg.clone());
6245                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6246            }
6247        }
6248        AstMutateCommand::EditVarInitialValue { value } => {
6249            // We target the SketchVar itself (matched by NodePath) rather than
6250            // the inner NumericLiteral so we can also write back into vars that
6251            // were declared without an initial value (e.g. bare `var`).
6252            if let NodeMut::SketchVar(sketch_var) = node {
6253                let Ok(literal) = to_source_number(*value) else {
6254                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6255                        "Could not convert number to AST literal: {:?}",
6256                        *value
6257                    ))));
6258                };
6259                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6260                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6261            }
6262        }
6263        AstMutateCommand::DeleteNode => {
6264            return TraversalReturn {
6265                mutate_body_item: MutateBodyItem::Delete,
6266                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6267            };
6268        }
6269    }
6270    TraversalReturn::new_continue(())
6271}
6272
6273struct FindSketchBlockSourceRange {
6274    /// The source range of the sketch block before mutation.
6275    target_before_mutation: SourceRange,
6276    /// The source range of the sketch block's last body item after mutation. We
6277    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6278    /// shared reference.
6279    found: Cell<Option<AstNodeRef>>,
6280}
6281
6282impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6283    type Error = crate::front::Error;
6284
6285    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6286        let Ok(node_range) = SourceRange::try_from(&node) else {
6287            return Ok(true);
6288        };
6289
6290        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6291            if node_range.module_id() == self.target_before_mutation.module_id()
6292                && node_range.start() == self.target_before_mutation.start()
6293                // End shouldn't match since we added something.
6294                && node_range.end() >= self.target_before_mutation.end()
6295            {
6296                self.found.set(sketch_block.body.items.last().map(|item| match item {
6297                    // For declarations like `circle1 = circle(...)`, use
6298                    // the init expression range so lookup in source_range_to_object
6299                    // matches the segment source range.
6300                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6301                    _ => AstNodeRef::from(item),
6302                }));
6303                return Ok(false);
6304            } else {
6305                // We found a different sketch block. No need to descend into
6306                // its children since sketch blocks cannot be nested.
6307                return Ok(true);
6308            }
6309        }
6310
6311        for child in node.children().iter() {
6312            if !child.visit(*self)? {
6313                return Ok(false);
6314            }
6315        }
6316
6317        Ok(true)
6318    }
6319}
6320
6321struct FindSketchBlockByNodePath {
6322    /// The Node Path of the sketch block before mutation.
6323    target_node_path: ast::NodePath,
6324    /// The ref of the sketch block's last body item after mutation. We need to
6325    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6326    /// reference.
6327    found: Cell<Option<AstNodeRef>>,
6328}
6329
6330impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6331    type Error = crate::front::Error;
6332
6333    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6334        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6335            return Ok(true);
6336        };
6337
6338        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6339            if let Some(node_path) = node_path
6340                && node_path == self.target_node_path
6341            {
6342                self.found.set(sketch_block.body.items.last().map(|item| match item {
6343                    // For declarations like `circle1 = circle(...)`, use
6344                    // the init expression range so lookup in source_range_to_object
6345                    // matches the segment source range.
6346                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6347                    _ => AstNodeRef::from(item),
6348                }));
6349
6350                return Ok(false);
6351            } else {
6352                // We found a different sketch block. No need to descend into
6353                // its children since sketch blocks cannot be nested.
6354                return Ok(true);
6355            }
6356        }
6357
6358        for child in node.children().iter() {
6359            if !child.visit(*self)? {
6360                return Ok(false);
6361            }
6362        }
6363
6364        Ok(true)
6365    }
6366}
6367
6368/// After adding an item to a sketch block, find the sketch block, and get the
6369/// source range of the added item. We assume that the added item is the last
6370/// item in the sketch block and that the sketch block's source range has grown,
6371/// but not moved from its starting offset.
6372///
6373/// TODO: Do we need to format *before* mutation in case formatting moves the
6374/// sketch block forward?
6375fn find_sketch_block_added_item(
6376    ast: &ast::Node<ast::Program>,
6377    sketch_block_before_mutation: &AstNodeRef,
6378) -> Result<AstNodeRef, KclError> {
6379    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6380        let find = FindSketchBlockByNodePath {
6381            target_node_path: node_path.clone(),
6382            found: Cell::new(None),
6383        };
6384        let node = crate::walk::Node::from(ast);
6385        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6386        find.found.into_inner().ok_or_else(|| {
6387            KclError::refactor(format!(
6388                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6389            ))
6390        })
6391    } else {
6392        // No NodePath. Fall back to legacy source range.
6393        let find = FindSketchBlockSourceRange {
6394            target_before_mutation: sketch_block_before_mutation.range,
6395            found: Cell::new(None),
6396        };
6397        let node = crate::walk::Node::from(ast);
6398        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6399        find.found.into_inner().ok_or_else(|| KclError::refactor(
6400            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?"),
6401        ))
6402    }
6403}
6404
6405fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6406    let message = error.message().trim();
6407    let message = if message.is_empty() {
6408        "unknown parse error"
6409    } else {
6410        message
6411    };
6412
6413    format!("{prefix}: {message}")
6414}
6415
6416fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6417    let (program, errors) = Program::parse(source).map_err(|err| {
6418        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6419    })?;
6420    if !errors.is_empty() {
6421        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6422            format_compilation_issues(parse_error_prefix, &errors),
6423        )));
6424    }
6425
6426    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6427}
6428
6429fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6430    let Some(first_issue) = issues
6431        .iter()
6432        .find(|issue| issue.severity.is_err())
6433        .or_else(|| issues.first())
6434    else {
6435        return prefix.to_owned();
6436    };
6437
6438    let message = first_issue.message.trim();
6439    let message = if message.is_empty() {
6440        "unknown parse error"
6441    } else {
6442        message
6443    };
6444
6445    if issues.len() > 1 {
6446        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6447    } else {
6448        format!("{prefix}: {message}")
6449    }
6450}
6451
6452fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6453    // TODO: Don't duplicate this from lib.rs Program.
6454    ast.recast_top(&Default::default(), 0)
6455}
6456
6457struct FindNumericLiteral {
6458    target: SourceRange,
6459    found: Cell<Option<ast::NumericLiteral>>,
6460}
6461
6462impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6463    type Error = crate::front::Error;
6464
6465    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6466        let Ok(node_range) = SourceRange::try_from(&node) else {
6467            return Ok(true);
6468        };
6469
6470        if node_range == self.target
6471            && let crate::walk::Node::NumericLiteral(literal) = node
6472        {
6473            self.found.set(Some(literal.inner.clone()));
6474            return Ok(false);
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
6487fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6488    let find = FindNumericLiteral {
6489        target,
6490        found: Cell::new(None),
6491    };
6492    let node = crate::walk::Node::from(ast);
6493    node.visit(&find).ok()?;
6494    find.found.into_inner()
6495}
6496
6497struct FindSketchVarInitialByNodePath<'a> {
6498    target: &'a ast::NodePath,
6499    sketch_var_found: Cell<bool>,
6500    initial_literal: Cell<Option<ast::NumericLiteral>>,
6501}
6502
6503impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6504    type Error = crate::front::Error;
6505
6506    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6507        if let crate::walk::Node::SketchVar(sketch_var) = node
6508            && sketch_var.node_path.as_ref() == Some(self.target)
6509        {
6510            self.sketch_var_found.set(true);
6511            if let Some(initial) = &sketch_var.initial {
6512                self.initial_literal.set(Some(initial.inner.clone()));
6513            }
6514            return Ok(false);
6515        }
6516
6517        for child in node.children().iter() {
6518            if !child.visit(*self)? {
6519                return Ok(false);
6520            }
6521        }
6522
6523        Ok(true)
6524    }
6525}
6526
6527/// Locate the source `var` declaration corresponding to a sketch-var solution.
6528///
6529/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6530/// from "target found." The inner [`Option`] is the initial numeric literal of
6531/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6532///
6533/// When `node_path` is `None` (e.g. for older outcomes that predate the
6534/// node-path propagation), this falls back to source-range matching, which
6535/// can break under whitespace shifts elsewhere in the file.
6536fn numeric_literal_at_node_path(
6537    ast: &ast::Node<ast::Program>,
6538    node_path: Option<&ast::NodePath>,
6539    source_range: SourceRange,
6540) -> Option<Option<ast::NumericLiteral>> {
6541    let Some(node_path) = node_path else {
6542        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";
6543        #[cfg(target_arch = "wasm32")]
6544        web_sys::console::warn_1(&message.into());
6545        #[cfg(not(target_arch = "wasm32"))]
6546        eprintln!("WARNING: {message}");
6547        return numeric_literal_at_source_range(ast, source_range).map(Some);
6548    };
6549    let find = FindSketchVarInitialByNodePath {
6550        target: node_path,
6551        sketch_var_found: Cell::new(false),
6552        initial_literal: Cell::new(None),
6553    };
6554    let node = crate::walk::Node::from(ast);
6555    node.visit(&find).ok()?;
6556    if !find.sketch_var_found.get() {
6557        return None;
6558    }
6559    Some(find.initial_literal.into_inner())
6560}
6561
6562fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6563    match suffix {
6564        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6565        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6566        NumericSuffix::M => Some(UnitLength::Meters),
6567        NumericSuffix::Inch => Some(UnitLength::Inches),
6568        NumericSuffix::Ft => Some(UnitLength::Feet),
6569        NumericSuffix::Yd => Some(UnitLength::Yards),
6570        _ => None,
6571    }
6572}
6573
6574fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6575    match suffix_length_unit(number.units) {
6576        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6577        None => number.value,
6578    }
6579}
6580
6581fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6582    match suffix_length_unit(literal.suffix) {
6583        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6584        None => literal.value,
6585    }
6586}
6587
6588fn var_solution_needs_commit(
6589    current_literal: &ast::NumericLiteral,
6590    solved_value: Number,
6591    default_length_unit: UnitLength,
6592) -> bool {
6593    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6594    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6595
6596    (current - solved).abs() > 1e-9
6597}
6598
6599fn preserve_var_solution_literal_style(
6600    current_literal: &ast::NumericLiteral,
6601    solved_value: Number,
6602    default_length_unit: UnitLength,
6603) -> Number {
6604    if current_literal.suffix == NumericSuffix::None {
6605        return Number {
6606            value: number_value_in_default_length_units(solved_value, default_length_unit),
6607            units: NumericSuffix::None,
6608        };
6609    }
6610
6611    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6612        return solved_value;
6613    };
6614
6615    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6616    Number {
6617        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6618        units: current_literal.suffix,
6619    }
6620}
6621
6622pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6623    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6624        inner: ast::ArrayExpression {
6625            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6626            non_code_meta: Default::default(),
6627            digest: None,
6628        },
6629        start: Default::default(),
6630        end: Default::default(),
6631        module_id: Default::default(),
6632        node_path: None,
6633        outer_attrs: Default::default(),
6634        pre_comments: Default::default(),
6635        comment_start: Default::default(),
6636    })))
6637}
6638
6639pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6640    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6641        ast::ArrayExpression {
6642            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6643            digest: None,
6644            non_code_meta: Default::default(),
6645        },
6646    ))))
6647}
6648
6649fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6650    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6651        ast::ArrayExpression {
6652            elements: vec![
6653                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6654                    point.x,
6655                )?)))),
6656                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6657                    point.y,
6658                )?)))),
6659            ],
6660            non_code_meta: Default::default(),
6661            digest: None,
6662        },
6663    ))))
6664}
6665
6666fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6667    match expr {
6668        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6669            inner: ast::Literal::from(to_source_number(*number)?),
6670            start: Default::default(),
6671            end: Default::default(),
6672            module_id: Default::default(),
6673            node_path: None,
6674            outer_attrs: Default::default(),
6675            pre_comments: Default::default(),
6676            comment_start: Default::default(),
6677        }))),
6678        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6679            inner: ast::SketchVar {
6680                initial: Some(Box::new(ast::Node {
6681                    inner: to_source_number(*number)?,
6682                    start: Default::default(),
6683                    end: Default::default(),
6684                    module_id: Default::default(),
6685                    node_path: None,
6686                    outer_attrs: Default::default(),
6687                    pre_comments: Default::default(),
6688                    comment_start: Default::default(),
6689                })),
6690                digest: None,
6691            },
6692            start: Default::default(),
6693            end: Default::default(),
6694            module_id: Default::default(),
6695            node_path: None,
6696            outer_attrs: Default::default(),
6697            pre_comments: Default::default(),
6698            comment_start: Default::default(),
6699        }))),
6700        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6701    }
6702}
6703
6704fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6705    Ok(ast::NumericLiteral {
6706        value: number.value,
6707        suffix: number.units,
6708        raw: format_number_literal(number.value, number.units, None)?,
6709        digest: None,
6710    })
6711}
6712
6713pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6714    ast::Expr::Name(Box::new(ast_name(name)))
6715}
6716
6717fn ast_name(name: String) -> ast::Node<ast::Name> {
6718    ast::Node {
6719        inner: ast::Name {
6720            name: ast::Node {
6721                inner: ast::Identifier { name, digest: None },
6722                start: Default::default(),
6723                end: Default::default(),
6724                module_id: Default::default(),
6725                node_path: None,
6726                outer_attrs: Default::default(),
6727                pre_comments: Default::default(),
6728                comment_start: Default::default(),
6729            },
6730            path: Vec::new(),
6731            abs_path: false,
6732            digest: None,
6733        },
6734        start: Default::default(),
6735        end: Default::default(),
6736        module_id: Default::default(),
6737        node_path: None,
6738        outer_attrs: Default::default(),
6739        pre_comments: Default::default(),
6740        comment_start: Default::default(),
6741    }
6742}
6743
6744pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6745    ast::Name {
6746        name: ast::Node {
6747            inner: ast::Identifier {
6748                name: name.to_owned(),
6749                digest: None,
6750            },
6751            start: Default::default(),
6752            end: Default::default(),
6753            module_id: Default::default(),
6754            node_path: None,
6755            outer_attrs: Default::default(),
6756            pre_comments: Default::default(),
6757            comment_start: Default::default(),
6758        },
6759        path: Default::default(),
6760        abs_path: false,
6761        digest: None,
6762    }
6763}
6764
6765// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6766
6767/// Create an AST node for coincident([expr1, expr2, ...])
6768pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6769    let elements = exprs.into_iter().collect::<Vec<_>>();
6770    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6771
6772    // Create array [expr1, expr2, ...]
6773    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6774        elements,
6775        digest: None,
6776        non_code_meta: Default::default(),
6777    })));
6778
6779    // Create coincident([...])
6780    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6781        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6782        unlabeled: Some(array_expr),
6783        arguments: Default::default(),
6784        digest: None,
6785        non_code_meta: Default::default(),
6786    })))
6787}
6788
6789/// Create an AST node for line(start = [...], end = [...])
6790pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6791    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6792        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6793        unlabeled: None,
6794        arguments: vec![
6795            ast::LabeledArg {
6796                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6797                arg: start_ast,
6798            },
6799            ast::LabeledArg {
6800                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6801                arg: end_ast,
6802            },
6803        ],
6804        digest: None,
6805        non_code_meta: Default::default(),
6806    })))
6807}
6808
6809/// Create an AST node for arc(start = [...], end = [...], center = [...])
6810pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6811    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6812        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6813        unlabeled: None,
6814        arguments: vec![
6815            ast::LabeledArg {
6816                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6817                arg: start_ast,
6818            },
6819            ast::LabeledArg {
6820                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6821                arg: end_ast,
6822            },
6823            ast::LabeledArg {
6824                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6825                arg: center_ast,
6826            },
6827        ],
6828        digest: None,
6829        non_code_meta: Default::default(),
6830    })))
6831}
6832
6833/// Create an AST node for circle(start = [...], center = [...])
6834pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6835    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6836        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6837        unlabeled: None,
6838        arguments: vec![
6839            ast::LabeledArg {
6840                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6841                arg: start_ast,
6842            },
6843            ast::LabeledArg {
6844                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6845                arg: center_ast,
6846            },
6847        ],
6848        digest: None,
6849        non_code_meta: Default::default(),
6850    })))
6851}
6852
6853/// Create an AST node for horizontal(line)
6854pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6855    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6856        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6857        unlabeled: Some(line_expr),
6858        arguments: Default::default(),
6859        digest: None,
6860        non_code_meta: Default::default(),
6861    })))
6862}
6863
6864/// Create an AST node for vertical(line)
6865pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6866    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6867        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6868        unlabeled: Some(line_expr),
6869        arguments: Default::default(),
6870        digest: None,
6871        non_code_meta: Default::default(),
6872    })))
6873}
6874
6875/// Create a member expression like object.property (e.g., line1.end)
6876pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6877    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6878        object: object_expr,
6879        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6880            name: ast::Node::no_src(ast::Identifier {
6881                name: property.to_string(),
6882                digest: None,
6883            }),
6884            path: Vec::new(),
6885            abs_path: false,
6886            digest: None,
6887        }))),
6888        computed: false,
6889        digest: None,
6890    })))
6891}
6892
6893pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6894    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6895        object: object_expr,
6896        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6897            value: index as f64,
6898            suffix: NumericSuffix::None,
6899            raw: index.to_string(),
6900            digest: None,
6901        })))),
6902        computed: true,
6903        digest: None,
6904    })))
6905}
6906
6907/// Create an AST node for `fixed([point, [x, y]])`.
6908fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6909    // Create [x, y] array literal.
6910    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6911        position.x,
6912    )?))));
6913    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6914        position.y,
6915    )?))));
6916    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6917        elements: vec![x_literal, y_literal],
6918        digest: None,
6919        non_code_meta: Default::default(),
6920    })));
6921
6922    // Create [point, [x, y]] outer array.
6923    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6924        elements: vec![point_expr, point_array],
6925        digest: None,
6926        non_code_meta: Default::default(),
6927    })));
6928
6929    // Create fixed([...])
6930    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6931        ast::CallExpressionKw {
6932            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6933            unlabeled: Some(array_expr),
6934            arguments: Default::default(),
6935            digest: None,
6936            non_code_meta: Default::default(),
6937        },
6938    ))))
6939}
6940
6941/// Create an AST node for equalLength([line1, line2, ...])
6942pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6943    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6944        elements: line_exprs,
6945        digest: None,
6946        non_code_meta: Default::default(),
6947    })));
6948
6949    // Create equalLength([...])
6950    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6951        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6952        unlabeled: Some(array_expr),
6953        arguments: Default::default(),
6954        digest: None,
6955        non_code_meta: Default::default(),
6956    })))
6957}
6958
6959/// Create an AST node for equalRadius([seg1, seg2, ...])
6960pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6961    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6962        elements: segment_exprs,
6963        digest: None,
6964        non_code_meta: Default::default(),
6965    })));
6966
6967    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6968        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6969        unlabeled: Some(array_expr),
6970        arguments: Default::default(),
6971        digest: None,
6972        non_code_meta: Default::default(),
6973    })))
6974}
6975
6976/// Create an AST node for tangent([seg1, seg2])
6977pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6978    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6979        elements: vec![seg1_expr, seg2_expr],
6980        digest: None,
6981        non_code_meta: Default::default(),
6982    })));
6983
6984    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6985        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6986        unlabeled: Some(array_expr),
6987        arguments: Default::default(),
6988        digest: None,
6989        non_code_meta: Default::default(),
6990    })))
6991}
6992
6993/// Create an AST node for symmetric([input1, input2], axis = line)
6994pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6995    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6996        elements: input_exprs,
6997        digest: None,
6998        non_code_meta: Default::default(),
6999    })));
7000    let arguments = vec![ast::LabeledArg {
7001        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7002        arg: axis_expr,
7003    }];
7004
7005    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7006        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7007        unlabeled: Some(array_expr),
7008        arguments,
7009        digest: None,
7010        non_code_meta: Default::default(),
7011    })))
7012}
7013
7014/// Create an AST node for midpoint(segment, point = point)
7015pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7016    let arguments = vec![ast::LabeledArg {
7017        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7018        arg: point_expr,
7019    }];
7020
7021    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
7022        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7023        unlabeled: Some(segment_expr),
7024        arguments,
7025        digest: None,
7026        non_code_meta: Default::default(),
7027    })))
7028}
7029
7030#[cfg(test)]
7031mod tests {
7032    use std::sync;
7033
7034    use super::*;
7035    use crate::engine::PlaneName;
7036    use crate::engine::engine_manager::EngineManager;
7037    use crate::execution::cache::SketchModeState;
7038    use crate::execution::cache::clear_mem_cache;
7039    use crate::execution::cache::read_old_memory;
7040    use crate::execution::cache::write_old_memory;
7041    use crate::front::Distance;
7042    use crate::front::Fixed;
7043    use crate::front::FixedPoint;
7044    use crate::front::Midpoint;
7045    use crate::front::Object;
7046    use crate::front::Plane;
7047    use crate::front::Sketch;
7048    use crate::front::Tangent;
7049    use crate::frontend::sketch::Vertical;
7050    use crate::pretty::NumericSuffix;
7051
7052    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7053        for object in &scene_graph.objects {
7054            if let ObjectKind::Sketch(_) = &object.kind {
7055                return Some(object);
7056            }
7057        }
7058        None
7059    }
7060
7061    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7062        for object in &scene_graph.objects {
7063            if let ObjectKind::Face(_) = &object.kind {
7064                return Some(object);
7065            }
7066        }
7067        None
7068    }
7069
7070    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7071        for object in &scene_graph.objects {
7072            if matches!(&object.kind, ObjectKind::Wall(_)) {
7073                return Some(object.id);
7074            }
7075        }
7076        None
7077    }
7078
7079    fn find_cap_object_id_with_solid_output_index(
7080        scene_graph: &SceneGraph,
7081        cap_kind: crate::frontend::api::CapKind,
7082        solid_output_index: usize,
7083    ) -> Option<ObjectId> {
7084        for object in &scene_graph.objects {
7085            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7086            {
7087                return Some(object.id);
7088            }
7089        }
7090        None
7091    }
7092
7093    #[test]
7094    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7095        let source = "\
7096region001 = region(point = [0.1, 0.1], sketch = s)
7097extrude001 = extrude(region001, length = 5)
7098revolve001 = revolve(region001, axis = Y)
7099sweep001 = sweep(region001, path = path001)
7100loft001 = loft(region001)
7101not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7102";
7103
7104        let program = Program::parse(source).unwrap().0.unwrap();
7105
7106        assert_eq!(
7107            region_name_from_sweep_variable(&program.ast, "extrude001"),
7108            Some("region001".to_owned())
7109        );
7110        assert_eq!(
7111            region_name_from_sweep_variable(&program.ast, "revolve001"),
7112            Some("region001".to_owned())
7113        );
7114        assert_eq!(
7115            region_name_from_sweep_variable(&program.ast, "sweep001"),
7116            Some("region001".to_owned())
7117        );
7118        assert_eq!(
7119            region_name_from_sweep_variable(&program.ast, "loft001"),
7120            Some("region001".to_owned())
7121        );
7122        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7123    }
7124
7125    #[track_caller]
7126    fn expect_sketch(object: &Object) -> &Sketch {
7127        if let ObjectKind::Sketch(sketch) = &object.kind {
7128            sketch
7129        } else {
7130            panic!("Object is not a sketch: {:?}", object);
7131        }
7132    }
7133
7134    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7135        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7136        let ObjectKind::Segment {
7137            segment: Segment::Point(point),
7138        } = &point_object.kind
7139        else {
7140            panic!("Object is not a point segment: {point_object:?}");
7141        };
7142        point.position.clone()
7143    }
7144
7145    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7146        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7147        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7148    }
7149
7150    /// Build a millimeter-valued point expression for concise sketch edit test
7151    /// setup.
7152    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7153        Point2d {
7154            x: Expr::Var(Number {
7155                value: x,
7156                units: NumericSuffix::Mm,
7157            }),
7158            y: Expr::Var(Number {
7159                value: y,
7160                units: NumericSuffix::Mm,
7161            }),
7162        }
7163    }
7164
7165    /// Build a millimeter-valued numeric point for comparing solved scene graph
7166    /// positions.
7167    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7168        Point2d {
7169            x: Number {
7170                value: x,
7171                units: NumericSuffix::Mm,
7172            },
7173            y: Number {
7174                value: y,
7175                units: NumericSuffix::Mm,
7176            },
7177        }
7178    }
7179
7180    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7181        LineCtor {
7182            start: Point2d {
7183                x: Expr::Number(Number { value: start_x, units }),
7184                y: Expr::Number(Number { value: start_y, units }),
7185            },
7186            end: Point2d {
7187                x: Expr::Number(Number { value: end_x, units }),
7188                y: Expr::Number(Number { value: end_y, units }),
7189            },
7190            construction: None,
7191        }
7192    }
7193
7194    async fn create_sketch_with_single_line(
7195        frontend: &mut FrontendState,
7196        ctx: &ExecutorContext,
7197        mock_ctx: &ExecutorContext,
7198        version: Version,
7199    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7200        frontend.program = Program::empty();
7201
7202        let sketch_args = SketchCtor {
7203            on: Plane::Default(PlaneName::Xy),
7204        };
7205        let (_src_delta, _scene_delta, sketch_id) = frontend
7206            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7207            .await
7208            .unwrap();
7209
7210        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7211        let (source_delta, scene_graph_delta) = frontend
7212            .add_segment(mock_ctx, version, sketch_id, segment, None)
7213            .await
7214            .unwrap();
7215        let line_id = *scene_graph_delta
7216            .new_objects
7217            .last()
7218            .expect("Expected line object id to be created");
7219
7220        (sketch_id, line_id, source_delta, scene_graph_delta)
7221    }
7222
7223    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7224        frontend.program = program.clone();
7225        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7226        frontend.update_state_after_exec(outcome, true);
7227    }
7228
7229    #[test]
7230    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7231        for (source, expected_message) in [
7232            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7233            ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7234        ] {
7235            let err = parse_frontend_mutation_source(
7236                source,
7237                "Error parsing KCL source after editing",
7238                "No AST produced after editing",
7239            )
7240            .expect_err("expected invalid KCL source to fail");
7241            let message = err.error.message();
7242
7243            assert_eq!(message, expected_message);
7244            assert!(!message.contains("CompilationIssue"));
7245            assert!(!message.contains("KclErrorDetails"));
7246            assert!(!message.contains("source_range"));
7247        }
7248    }
7249
7250    #[tokio::test(flavor = "multi_thread")]
7251    async fn test_edit_constraint_parse_error_messages_are_user_facing() {
7252        let initial_source = "\
7253sketch(on = XY) {
7254  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7255  distance([line1.start, line1.end]) == 10
7256}
7257";
7258        let program = Program::parse(initial_source).unwrap().0.unwrap();
7259
7260        let mut frontend = FrontendState::new();
7261        let mock_ctx = ExecutorContext::new_mock(None).await;
7262        let version = Version(0);
7263
7264        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7265        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7266        let sketch_id = sketch_object.id;
7267        let sketch = expect_sketch(sketch_object);
7268        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7269
7270        for (value, expected_message) in [
7271            ("**", "Invalid constraint value: Unexpected token: *"),
7272            ("3'", "Invalid constraint value: found unknown token '''"),
7273        ] {
7274            let err = frontend
7275                .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7276                .await
7277                .expect_err("expected invalid constraint expression to fail");
7278            let message = err.error.message();
7279
7280            assert_eq!(message, expected_message);
7281            assert!(!message.contains("CompilationIssue"));
7282            assert!(!message.contains("KclErrorDetails"));
7283            assert!(!message.contains("source_range"));
7284        }
7285
7286        mock_ctx.close().await;
7287    }
7288
7289    #[tokio::test(flavor = "multi_thread")]
7290    async fn test_failed_edit_constraint_does_not_update_program() {
7291        let initial_source = "\
7292sketch(on = XY) {
7293  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7294  distance([line1.start, line1.end]) == 10
7295}
7296";
7297        let program = Program::parse(initial_source).unwrap().0.unwrap();
7298        let original_source = program.original_file_contents.clone();
7299
7300        let mut frontend = FrontendState::new();
7301        let mock_ctx = ExecutorContext::new_mock(None).await;
7302        let version = Version(0);
7303
7304        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7305        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7306        let sketch_id = sketch_object.id;
7307        let sketch = expect_sketch(sketch_object);
7308        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7309
7310        frontend
7311            .edit_constraint(
7312                &mock_ctx,
7313                version,
7314                sketch_id,
7315                constraint_id,
7316                "unknownDistance".to_owned(),
7317            )
7318            .await
7319            .expect_err("expected invalid constraint value to fail execution");
7320
7321        assert_eq!(frontend.program.original_file_contents, original_source);
7322        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7323
7324        mock_ctx.close().await;
7325    }
7326
7327    #[tokio::test(flavor = "multi_thread")]
7328    async fn test_edit_constraint_array_index_oob_fails_in_sketch_mode() {
7329        let initial_source = "\
7330arr = [0]
7331sketch(on = XY) {
7332  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7333  distance([line1.start, line1.end]) == 10
7334}
7335";
7336        let program = Program::parse(initial_source).unwrap().0.unwrap();
7337
7338        let mut frontend = FrontendState::new();
7339        let mock_ctx = ExecutorContext::new_mock(None).await;
7340        let version = Version(0);
7341
7342        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7343        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7344        let sketch_id = sketch_object.id;
7345        let sketch = expect_sketch(sketch_object);
7346        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7347
7348        // In sketch mode execution, an out-of-bounds array index is an error.
7349        // It should not fall back to the first element of the array the way
7350        // plain mock execution does.
7351        let err = frontend
7352            .edit_constraint(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7353            .await
7354            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7355        let message = err.error.message();
7356        assert!(
7357            message.contains("The array doesn't have any item at index 5"),
7358            "unexpected error message: {message}"
7359        );
7360
7361        mock_ctx.close().await;
7362    }
7363
7364    #[tokio::test(flavor = "multi_thread")]
7365    async fn test_sketch_checkpoint_round_trip_restores_state() {
7366        let mut frontend = FrontendState::new();
7367        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7368        let mock_ctx = ExecutorContext::new_mock(None).await;
7369        let version = Version(0);
7370
7371        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7372            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7373
7374        let expected_source = source_delta.text.clone();
7375        let expected_scene_graph = frontend.scene_graph.clone();
7376        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7377        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7378
7379        let checkpoint_id = frontend
7380            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7381            .await
7382            .unwrap();
7383
7384        let edited_segments = vec![ExistingSegmentCtor {
7385            id: line_id,
7386            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7387        }];
7388        let (edited_source, _edited_scene) = frontend
7389            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7390            .await
7391            .unwrap();
7392        assert_ne!(edited_source.text, expected_source);
7393
7394        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7395
7396        assert_eq!(restored.source_delta.text, expected_source);
7397        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7398        assert!(restored.scene_graph_delta.invalidates_ids);
7399        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7400        assert_eq!(frontend.scene_graph, expected_scene_graph);
7401        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7402
7403        ctx.close().await;
7404    }
7405
7406    #[tokio::test(flavor = "multi_thread")]
7407    async fn test_sketch_checkpoints_prune_oldest_entries() {
7408        let mut frontend = FrontendState::new();
7409        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7410        let mock_ctx = ExecutorContext::new_mock(None).await;
7411        let version = Version(0);
7412
7413        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7414            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7415
7416        let mut checkpoint_ids = Vec::new();
7417        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7418            checkpoint_ids.push(
7419                frontend
7420                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7421                    .await
7422                    .unwrap(),
7423            );
7424        }
7425
7426        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7427        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7428
7429        let oldest_retained = checkpoint_ids[3];
7430        assert_eq!(
7431            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7432            Some(oldest_retained)
7433        );
7434
7435        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7436        assert!(evicted_restore.is_err());
7437        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7438
7439        frontend
7440            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7441            .await
7442            .unwrap();
7443
7444        ctx.close().await;
7445    }
7446
7447    #[tokio::test(flavor = "multi_thread")]
7448    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7449        let mut frontend = FrontendState::new();
7450        let missing_checkpoint = SketchCheckpointId::new(999);
7451
7452        let err = frontend
7453            .restore_sketch_checkpoint(missing_checkpoint)
7454            .await
7455            .expect_err("Expected restore to fail for missing checkpoint");
7456
7457        assert!(err.msg.contains("Sketch checkpoint not found"));
7458    }
7459
7460    #[tokio::test(flavor = "multi_thread")]
7461    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7462        let mut frontend = FrontendState::new();
7463        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7464        let mock_ctx = ExecutorContext::new_mock(None).await;
7465        let version = Version(0);
7466
7467        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7468            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7469
7470        let checkpoint_a = frontend
7471            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7472            .await
7473            .unwrap();
7474        let checkpoint_b = frontend
7475            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7476            .await
7477            .unwrap();
7478        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7479
7480        frontend.clear_sketch_checkpoints();
7481        assert!(frontend.sketch_checkpoints.is_empty());
7482        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7483        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7484
7485        ctx.close().await;
7486    }
7487
7488    #[tokio::test(flavor = "multi_thread")]
7489    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7490        let mut frontend = FrontendState::new();
7491        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7492        let mock_ctx = ExecutorContext::new_mock(None).await;
7493        let version = Version(0);
7494
7495        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7496            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7497        let old_source = source_delta.text.clone();
7498        let old_checkpoint = frontend
7499            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7500            .await
7501            .unwrap();
7502        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7503
7504        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7505            .unwrap()
7506            .0
7507            .unwrap();
7508
7509        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7510        let SetProgramOutcome::Success {
7511            checkpoint_id: Some(new_checkpoint),
7512            ..
7513        } = result
7514        else {
7515            panic!("Expected Success with a fresh checkpoint baseline");
7516        };
7517
7518        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7519
7520        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7521        assert_eq!(old_restore.source_delta.text, old_source);
7522
7523        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7524        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7525
7526        ctx.close().await;
7527    }
7528
7529    #[tokio::test(flavor = "multi_thread")]
7530    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7531        let mut frontend = FrontendState::new();
7532        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7533        let mock_ctx = ExecutorContext::new_mock(None).await;
7534        let version = Version(0);
7535
7536        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7537            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7538        let old_checkpoint = frontend
7539            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7540            .await
7541            .unwrap();
7542        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7543
7544        let failing_program = Program::parse(
7545            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7546        )
7547        .unwrap()
7548        .0
7549        .unwrap();
7550
7551        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7552        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7553        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7554        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7555
7556        ctx.close().await;
7557    }
7558
7559    #[tokio::test(flavor = "multi_thread")]
7560    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7561        let mut frontend = FrontendState::new();
7562        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7563
7564        let program = Program::parse(
7565            "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",
7566        )
7567        .unwrap()
7568        .0
7569        .unwrap();
7570        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7571        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7572            panic!("Expected successful baseline program execution");
7573        };
7574
7575        clear_mem_cache().await;
7576        assert!(read_old_memory().await.is_none());
7577
7578        let checkpoint_without_mock_memory = frontend
7579            .create_sketch_checkpoint((*exec_outcome).clone())
7580            .await
7581            .unwrap();
7582
7583        write_old_memory(SketchModeState::new_for_tests()).await;
7584        assert!(read_old_memory().await.is_some());
7585
7586        let checkpoint_with_mock_memory = frontend
7587            .create_sketch_checkpoint((*exec_outcome).clone())
7588            .await
7589            .unwrap();
7590
7591        clear_mem_cache().await;
7592        assert!(read_old_memory().await.is_none());
7593
7594        frontend
7595            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7596            .await
7597            .unwrap();
7598        assert!(read_old_memory().await.is_some());
7599
7600        frontend
7601            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7602            .await
7603            .unwrap();
7604        assert!(read_old_memory().await.is_none());
7605
7606        ctx.close().await;
7607    }
7608
7609    #[tokio::test(flavor = "multi_thread")]
7610    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7611        let source = "\
7612sketch(on = XY) {
7613  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7614}
7615
7616bad = missing_name
7617";
7618        let program = Program::parse(source).unwrap().0.unwrap();
7619
7620        let mut frontend = FrontendState::new();
7621
7622        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7623        let mock_ctx = ExecutorContext::new_mock(None).await;
7624        let version = Version(0);
7625        let project_id = ProjectId(0);
7626        let file_id = FileId(0);
7627
7628        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7629            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7630        };
7631
7632        let sketch_id = frontend
7633            .scene_graph
7634            .objects
7635            .iter()
7636            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7637            .expect("Expected sketch object from errored hack_set_program");
7638
7639        frontend
7640            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7641            .await
7642            .unwrap();
7643
7644        ctx.close().await;
7645        mock_ctx.close().await;
7646    }
7647
7648    #[tokio::test(flavor = "multi_thread")]
7649    async fn test_new_sketch_add_point_edit_point() {
7650        let program = Program::empty();
7651
7652        let mut frontend = FrontendState::new();
7653        frontend.program = program;
7654
7655        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7656        let mock_ctx = ExecutorContext::new_mock(None).await;
7657        let version = Version(0);
7658
7659        let sketch_args = SketchCtor {
7660            on: Plane::Default(PlaneName::Xy),
7661        };
7662        let (_src_delta, scene_delta, sketch_id) = frontend
7663            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7664            .await
7665            .unwrap();
7666        assert_eq!(sketch_id, ObjectId(1));
7667        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7668        let sketch_object = &scene_delta.new_graph.objects[1];
7669        assert_eq!(sketch_object.id, ObjectId(1));
7670        assert_eq!(
7671            sketch_object.kind,
7672            ObjectKind::Sketch(Sketch {
7673                args: SketchCtor {
7674                    on: Plane::Default(PlaneName::Xy)
7675                },
7676                plane: ObjectId(0),
7677                segments: vec![],
7678                constraints: vec![],
7679            })
7680        );
7681        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7682
7683        let point_ctor = PointCtor {
7684            position: Point2d {
7685                x: Expr::Number(Number {
7686                    value: 1.0,
7687                    units: NumericSuffix::Inch,
7688                }),
7689                y: Expr::Number(Number {
7690                    value: 2.0,
7691                    units: NumericSuffix::Inch,
7692                }),
7693            },
7694        };
7695        let segment = SegmentCtor::Point(point_ctor);
7696        let (src_delta, scene_delta) = frontend
7697            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7698            .await
7699            .unwrap();
7700        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
7701        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7702        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7703        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7704            assert_eq!(scene_object.id.0, i);
7705        }
7706
7707        let point_id = *scene_delta.new_objects.last().unwrap();
7708
7709        let point_ctor = PointCtor {
7710            position: Point2d {
7711                x: Expr::Number(Number {
7712                    value: 3.0,
7713                    units: NumericSuffix::Inch,
7714                }),
7715                y: Expr::Number(Number {
7716                    value: 4.0,
7717                    units: NumericSuffix::Inch,
7718                }),
7719            },
7720        };
7721        let segments = vec![ExistingSegmentCtor {
7722            id: point_id,
7723            ctor: SegmentCtor::Point(point_ctor),
7724        }];
7725        let (src_delta, scene_delta) = frontend
7726            .edit_segments(&mock_ctx, version, sketch_id, segments)
7727            .await
7728            .unwrap();
7729        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
7730        assert_eq!(scene_delta.new_objects, vec![]);
7731        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7732
7733        ctx.close().await;
7734        mock_ctx.close().await;
7735    }
7736
7737    #[tokio::test(flavor = "multi_thread")]
7738    async fn test_new_sketch_add_line_edit_line() {
7739        let program = Program::empty();
7740
7741        let mut frontend = FrontendState::new();
7742        frontend.program = program;
7743
7744        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7745        let mock_ctx = ExecutorContext::new_mock(None).await;
7746        let version = Version(0);
7747
7748        let sketch_args = SketchCtor {
7749            on: Plane::Default(PlaneName::Xy),
7750        };
7751        let (_src_delta, scene_delta, sketch_id) = frontend
7752            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7753            .await
7754            .unwrap();
7755        assert_eq!(sketch_id, ObjectId(1));
7756        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7757        let sketch_object = &scene_delta.new_graph.objects[1];
7758        assert_eq!(sketch_object.id, ObjectId(1));
7759        assert_eq!(
7760            sketch_object.kind,
7761            ObjectKind::Sketch(Sketch {
7762                args: SketchCtor {
7763                    on: Plane::Default(PlaneName::Xy)
7764                },
7765                plane: ObjectId(0),
7766                segments: vec![],
7767                constraints: vec![],
7768            })
7769        );
7770        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7771
7772        let line_ctor = LineCtor {
7773            start: Point2d {
7774                x: Expr::Number(Number {
7775                    value: 0.0,
7776                    units: NumericSuffix::Mm,
7777                }),
7778                y: Expr::Number(Number {
7779                    value: 0.0,
7780                    units: NumericSuffix::Mm,
7781                }),
7782            },
7783            end: Point2d {
7784                x: Expr::Number(Number {
7785                    value: 10.0,
7786                    units: NumericSuffix::Mm,
7787                }),
7788                y: Expr::Number(Number {
7789                    value: 10.0,
7790                    units: NumericSuffix::Mm,
7791                }),
7792            },
7793            construction: None,
7794        };
7795        let segment = SegmentCtor::Line(line_ctor);
7796        let (src_delta, scene_delta) = frontend
7797            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7798            .await
7799            .unwrap();
7800        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
7801        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7802        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7803        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7804            assert_eq!(scene_object.id.0, i);
7805        }
7806
7807        // The new objects are the end points and then the line.
7808        let line = *scene_delta.new_objects.last().unwrap();
7809
7810        let line_ctor = LineCtor {
7811            start: Point2d {
7812                x: Expr::Number(Number {
7813                    value: 1.0,
7814                    units: NumericSuffix::Mm,
7815                }),
7816                y: Expr::Number(Number {
7817                    value: 2.0,
7818                    units: NumericSuffix::Mm,
7819                }),
7820            },
7821            end: Point2d {
7822                x: Expr::Number(Number {
7823                    value: 13.0,
7824                    units: NumericSuffix::Mm,
7825                }),
7826                y: Expr::Number(Number {
7827                    value: 14.0,
7828                    units: NumericSuffix::Mm,
7829                }),
7830            },
7831            construction: None,
7832        };
7833        let segments = vec![ExistingSegmentCtor {
7834            id: line,
7835            ctor: SegmentCtor::Line(line_ctor),
7836        }];
7837        let (src_delta, scene_delta) = frontend
7838            .edit_segments(&mock_ctx, version, sketch_id, segments)
7839            .await
7840            .unwrap();
7841        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
7842        assert_eq!(scene_delta.new_objects, vec![]);
7843        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7844
7845        ctx.close().await;
7846        mock_ctx.close().await;
7847    }
7848
7849    #[tokio::test(flavor = "multi_thread")]
7850    async fn test_new_sketch_add_arc_edit_arc() {
7851        let program = Program::empty();
7852
7853        let mut frontend = FrontendState::new();
7854        frontend.program = program;
7855
7856        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7857        let mock_ctx = ExecutorContext::new_mock(None).await;
7858        let version = Version(0);
7859
7860        let sketch_args = SketchCtor {
7861            on: Plane::Default(PlaneName::Xy),
7862        };
7863        let (_src_delta, scene_delta, sketch_id) = frontend
7864            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7865            .await
7866            .unwrap();
7867        assert_eq!(sketch_id, ObjectId(1));
7868        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7869        let sketch_object = &scene_delta.new_graph.objects[1];
7870        assert_eq!(sketch_object.id, ObjectId(1));
7871        assert_eq!(
7872            sketch_object.kind,
7873            ObjectKind::Sketch(Sketch {
7874                args: SketchCtor {
7875                    on: Plane::Default(PlaneName::Xy),
7876                },
7877                plane: ObjectId(0),
7878                segments: vec![],
7879                constraints: vec![],
7880            })
7881        );
7882        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7883
7884        let arc_ctor = ArcCtor {
7885            start: Point2d {
7886                x: Expr::Var(Number {
7887                    value: 0.0,
7888                    units: NumericSuffix::Mm,
7889                }),
7890                y: Expr::Var(Number {
7891                    value: 0.0,
7892                    units: NumericSuffix::Mm,
7893                }),
7894            },
7895            end: Point2d {
7896                x: Expr::Var(Number {
7897                    value: 10.0,
7898                    units: NumericSuffix::Mm,
7899                }),
7900                y: Expr::Var(Number {
7901                    value: 10.0,
7902                    units: NumericSuffix::Mm,
7903                }),
7904            },
7905            center: Point2d {
7906                x: Expr::Var(Number {
7907                    value: 10.0,
7908                    units: NumericSuffix::Mm,
7909                }),
7910                y: Expr::Var(Number {
7911                    value: 0.0,
7912                    units: NumericSuffix::Mm,
7913                }),
7914            },
7915            construction: None,
7916        };
7917        let segment = SegmentCtor::Arc(arc_ctor);
7918        let (src_delta, scene_delta) = frontend
7919            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7920            .await
7921            .unwrap();
7922        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
7923        assert_eq!(
7924            scene_delta.new_objects,
7925            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7926        );
7927        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7928            assert_eq!(scene_object.id.0, i);
7929        }
7930        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7931
7932        // The new objects are the end points, the center, and then the arc.
7933        let arc = *scene_delta.new_objects.last().unwrap();
7934
7935        let arc_ctor = ArcCtor {
7936            start: Point2d {
7937                x: Expr::Var(Number {
7938                    value: 1.0,
7939                    units: NumericSuffix::Mm,
7940                }),
7941                y: Expr::Var(Number {
7942                    value: 2.0,
7943                    units: NumericSuffix::Mm,
7944                }),
7945            },
7946            end: Point2d {
7947                x: Expr::Var(Number {
7948                    value: 13.0,
7949                    units: NumericSuffix::Mm,
7950                }),
7951                y: Expr::Var(Number {
7952                    value: 14.0,
7953                    units: NumericSuffix::Mm,
7954                }),
7955            },
7956            center: Point2d {
7957                x: Expr::Var(Number {
7958                    value: 13.0,
7959                    units: NumericSuffix::Mm,
7960                }),
7961                y: Expr::Var(Number {
7962                    value: 2.0,
7963                    units: NumericSuffix::Mm,
7964                }),
7965            },
7966            construction: None,
7967        };
7968        let segments = vec![ExistingSegmentCtor {
7969            id: arc,
7970            ctor: SegmentCtor::Arc(arc_ctor),
7971        }];
7972        let (src_delta, scene_delta) = frontend
7973            .edit_segments(&mock_ctx, version, sketch_id, segments)
7974            .await
7975            .unwrap();
7976        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
7977        assert_eq!(scene_delta.new_objects, vec![]);
7978        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7979
7980        ctx.close().await;
7981        mock_ctx.close().await;
7982    }
7983
7984    #[tokio::test(flavor = "multi_thread")]
7985    async fn test_new_sketch_add_circle_edit_circle() {
7986        let program = Program::empty();
7987
7988        let mut frontend = FrontendState::new();
7989        frontend.program = program;
7990
7991        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7992        let mock_ctx = ExecutorContext::new_mock(None).await;
7993        let version = Version(0);
7994
7995        let sketch_args = SketchCtor {
7996            on: Plane::Default(PlaneName::Xy),
7997        };
7998        let (_src_delta, _scene_delta, sketch_id) = frontend
7999            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8000            .await
8001            .unwrap();
8002
8003        // Add a circle segment.
8004        let circle_ctor = CircleCtor {
8005            start: Point2d {
8006                x: Expr::Var(Number {
8007                    value: 5.0,
8008                    units: NumericSuffix::Mm,
8009                }),
8010                y: Expr::Var(Number {
8011                    value: 0.0,
8012                    units: NumericSuffix::Mm,
8013                }),
8014            },
8015            center: Point2d {
8016                x: Expr::Var(Number {
8017                    value: 0.0,
8018                    units: NumericSuffix::Mm,
8019                }),
8020                y: Expr::Var(Number {
8021                    value: 0.0,
8022                    units: NumericSuffix::Mm,
8023                }),
8024            },
8025            construction: None,
8026        };
8027        let segment = SegmentCtor::Circle(circle_ctor);
8028        let (src_delta, scene_delta) = frontend
8029            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8030            .await
8031            .unwrap();
8032        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8033        // The new objects are start, center, and then the circle segment.
8034        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8035        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8036
8037        let circle = *scene_delta.new_objects.last().unwrap();
8038
8039        // Edit the circle segment.
8040        let circle_ctor = CircleCtor {
8041            start: Point2d {
8042                x: Expr::Var(Number {
8043                    value: 10.0,
8044                    units: NumericSuffix::Mm,
8045                }),
8046                y: Expr::Var(Number {
8047                    value: 0.0,
8048                    units: NumericSuffix::Mm,
8049                }),
8050            },
8051            center: Point2d {
8052                x: Expr::Var(Number {
8053                    value: 3.0,
8054                    units: NumericSuffix::Mm,
8055                }),
8056                y: Expr::Var(Number {
8057                    value: 4.0,
8058                    units: NumericSuffix::Mm,
8059                }),
8060            },
8061            construction: None,
8062        };
8063        let segments = vec![ExistingSegmentCtor {
8064            id: circle,
8065            ctor: SegmentCtor::Circle(circle_ctor),
8066        }];
8067        let (src_delta, scene_delta) = frontend
8068            .edit_segments(&mock_ctx, version, sketch_id, segments)
8069            .await
8070            .unwrap();
8071        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8072        assert_eq!(scene_delta.new_objects, vec![]);
8073        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8074
8075        ctx.close().await;
8076        mock_ctx.close().await;
8077    }
8078
8079    #[tokio::test(flavor = "multi_thread")]
8080    async fn test_delete_circle() {
8081        let initial_source = "sketch001 = sketch(on = XY) {
8082  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8083}
8084";
8085
8086        let program = Program::parse(initial_source).unwrap().0.unwrap();
8087        let mut frontend = FrontendState::new();
8088
8089        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8090        let mock_ctx = ExecutorContext::new_mock(None).await;
8091        let version = Version(0);
8092
8093        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8094        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8095        let sketch_id = sketch_object.id;
8096        let sketch = expect_sketch(sketch_object);
8097
8098        // The sketch should have 3 segments: start point, center point, and the circle.
8099        assert_eq!(sketch.segments.len(), 3);
8100        let circle_id = sketch.segments[2];
8101
8102        // Delete the circle.
8103        let (src_delta, scene_delta) = frontend
8104            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8105            .await
8106            .unwrap();
8107        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8108        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8109        let new_sketch = expect_sketch(new_sketch_object);
8110        assert_eq!(new_sketch.segments.len(), 0);
8111
8112        ctx.close().await;
8113        mock_ctx.close().await;
8114    }
8115
8116    #[tokio::test(flavor = "multi_thread")]
8117    async fn test_edit_circle_via_point() {
8118        let initial_source = "sketch001 = sketch(on = XY) {
8119  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8120}
8121";
8122
8123        let program = Program::parse(initial_source).unwrap().0.unwrap();
8124        let mut frontend = FrontendState::new();
8125
8126        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8127        let mock_ctx = ExecutorContext::new_mock(None).await;
8128        let version = Version(0);
8129
8130        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8131        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8132        let sketch_id = sketch_object.id;
8133        let sketch = expect_sketch(sketch_object);
8134
8135        // Find the circle segment and its start point.
8136        let circle_id = sketch
8137            .segments
8138            .iter()
8139            .copied()
8140            .find(|seg_id| {
8141                matches!(
8142                    &frontend.scene_graph.objects[seg_id.0].kind,
8143                    ObjectKind::Segment {
8144                        segment: Segment::Circle(_)
8145                    }
8146                )
8147            })
8148            .expect("Expected a circle segment in sketch");
8149        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8150        let ObjectKind::Segment {
8151            segment: Segment::Circle(circle),
8152        } = &circle_object.kind
8153        else {
8154            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8155        };
8156        let start_point_id = circle.start;
8157
8158        // Edit the start point via SegmentCtor::Point.
8159        let segments = vec![ExistingSegmentCtor {
8160            id: start_point_id,
8161            ctor: SegmentCtor::Point(PointCtor {
8162                position: Point2d {
8163                    x: Expr::Var(Number {
8164                        value: 7.0,
8165                        units: NumericSuffix::Mm,
8166                    }),
8167                    y: Expr::Var(Number {
8168                        value: 1.0,
8169                        units: NumericSuffix::Mm,
8170                    }),
8171                },
8172            }),
8173        }];
8174        let (src_delta, _scene_delta) = frontend
8175            .edit_segments(&mock_ctx, version, sketch_id, segments)
8176            .await
8177            .unwrap();
8178        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8179
8180        ctx.close().await;
8181        mock_ctx.close().await;
8182    }
8183
8184    #[tokio::test(flavor = "multi_thread")]
8185    async fn test_add_line_when_sketch_block_uses_variable() {
8186        let initial_source = "s = sketch(on = XY) {}
8187";
8188
8189        let program = Program::parse(initial_source).unwrap().0.unwrap();
8190
8191        let mut frontend = FrontendState::new();
8192
8193        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8194        let mock_ctx = ExecutorContext::new_mock(None).await;
8195        let version = Version(0);
8196
8197        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8198        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8199        let sketch_id = sketch_object.id;
8200
8201        let line_ctor = LineCtor {
8202            start: Point2d {
8203                x: Expr::Number(Number {
8204                    value: 0.0,
8205                    units: NumericSuffix::Mm,
8206                }),
8207                y: Expr::Number(Number {
8208                    value: 0.0,
8209                    units: NumericSuffix::Mm,
8210                }),
8211            },
8212            end: Point2d {
8213                x: Expr::Number(Number {
8214                    value: 10.0,
8215                    units: NumericSuffix::Mm,
8216                }),
8217                y: Expr::Number(Number {
8218                    value: 10.0,
8219                    units: NumericSuffix::Mm,
8220                }),
8221            },
8222            construction: None,
8223        };
8224        let segment = SegmentCtor::Line(line_ctor);
8225        let (src_delta, scene_delta) = frontend
8226            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8227            .await
8228            .unwrap();
8229        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8230        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8231        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8232
8233        ctx.close().await;
8234        mock_ctx.close().await;
8235    }
8236
8237    #[tokio::test(flavor = "multi_thread")]
8238    async fn test_new_sketch_add_line_delete_sketch() {
8239        let program = Program::empty();
8240
8241        let mut frontend = FrontendState::new();
8242        frontend.program = program;
8243
8244        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8245        let mock_ctx = ExecutorContext::new_mock(None).await;
8246        let version = Version(0);
8247
8248        let sketch_args = SketchCtor {
8249            on: Plane::Default(PlaneName::Xy),
8250        };
8251        let (_src_delta, scene_delta, sketch_id) = frontend
8252            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8253            .await
8254            .unwrap();
8255        assert_eq!(sketch_id, ObjectId(1));
8256        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8257        let sketch_object = &scene_delta.new_graph.objects[1];
8258        assert_eq!(sketch_object.id, ObjectId(1));
8259        assert_eq!(
8260            sketch_object.kind,
8261            ObjectKind::Sketch(Sketch {
8262                args: SketchCtor {
8263                    on: Plane::Default(PlaneName::Xy)
8264                },
8265                plane: ObjectId(0),
8266                segments: vec![],
8267                constraints: vec![],
8268            })
8269        );
8270        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8271
8272        let line_ctor = LineCtor {
8273            start: Point2d {
8274                x: Expr::Number(Number {
8275                    value: 0.0,
8276                    units: NumericSuffix::Mm,
8277                }),
8278                y: Expr::Number(Number {
8279                    value: 0.0,
8280                    units: NumericSuffix::Mm,
8281                }),
8282            },
8283            end: Point2d {
8284                x: Expr::Number(Number {
8285                    value: 10.0,
8286                    units: NumericSuffix::Mm,
8287                }),
8288                y: Expr::Number(Number {
8289                    value: 10.0,
8290                    units: NumericSuffix::Mm,
8291                }),
8292            },
8293            construction: None,
8294        };
8295        let segment = SegmentCtor::Line(line_ctor);
8296        let (src_delta, scene_delta) = frontend
8297            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8298            .await
8299            .unwrap();
8300        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8301        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8302
8303        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8304        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8305        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8306
8307        ctx.close().await;
8308        mock_ctx.close().await;
8309    }
8310
8311    #[tokio::test(flavor = "multi_thread")]
8312    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8313        let initial_source = "s = sketch(on = XY) {}
8314";
8315
8316        let program = Program::parse(initial_source).unwrap().0.unwrap();
8317
8318        let mut frontend = FrontendState::new();
8319
8320        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8321        let version = Version(0);
8322
8323        frontend.hack_set_program(&ctx, program).await.unwrap();
8324        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8325        let sketch_id = sketch_object.id;
8326
8327        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8328        insta::assert_snapshot!(
8329            "test_delete_sketch_when_sketch_block_uses_variable",
8330            src_delta.text.as_str()
8331        );
8332        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8333
8334        ctx.close().await;
8335    }
8336
8337    #[tokio::test(flavor = "multi_thread")]
8338    async fn test_delete_sketch_after_comment() {
8339        let initial_source = "sketch001 = sketch(on = XZ) {
8340}
8341";
8342
8343        let program = Program::parse(initial_source).unwrap().0.unwrap();
8344        let mut frontend = FrontendState::new();
8345
8346        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8347        let version = Version(0);
8348
8349        frontend.hack_set_program(&ctx, program).await.unwrap();
8350        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8351        let sketch_id = sketch_object.id;
8352        let original_source = sketch_object.source.clone();
8353
8354        let commented_source = "// test 1
8355sketch001 = sketch(on = XZ) {
8356}
8357";
8358        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8359        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8360
8361        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8362        assert_eq!(cached_sketch_object.source, original_source);
8363
8364        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8365        assert!(
8366            !src_delta.text.contains("sketch001"),
8367            "sketch was not deleted: {}",
8368            src_delta.text
8369        );
8370        // The leading line comment must survive deletion.
8371        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8372        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8373
8374        ctx.close().await;
8375    }
8376
8377    #[tokio::test(flavor = "multi_thread")]
8378    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8379        let initial_source = "sketch001 = sketch(on = XZ) {
8380}
8381foo = 1
8382";
8383
8384        let program = Program::parse(initial_source).unwrap().0.unwrap();
8385        let mut frontend = FrontendState::new();
8386
8387        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8388        let version = Version(0);
8389
8390        frontend.hack_set_program(&ctx, program).await.unwrap();
8391        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8392        let sketch_id = sketch_object.id;
8393
8394        let commented_source = "// keep me
8395sketch001 = sketch(on = XZ) {
8396}
8397foo = 1
8398";
8399        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8400        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8401
8402        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8403        // The leading comment should remain, now attached to the following body item.
8404        insta::assert_snapshot!(
8405            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8406            src_delta.text.as_str()
8407        );
8408
8409        ctx.close().await;
8410    }
8411
8412    #[tokio::test(flavor = "multi_thread")]
8413    async fn test_delete_segment_preserves_pre_comment() {
8414        let initial_source = "\
8415sketch(on = XY) {
8416  point(at = [var 1, var 2])
8417  // describe the middle point
8418  point(at = [var 3, var 4])
8419  point(at = [var 5, var 6])
8420}
8421";
8422
8423        let program = Program::parse(initial_source).unwrap().0.unwrap();
8424        let mut frontend = FrontendState::new();
8425
8426        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8427        let mock_ctx = ExecutorContext::new_mock(None).await;
8428        let version = Version(0);
8429
8430        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8431        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8432        let sketch_id = sketch_object.id;
8433        let sketch = expect_sketch(sketch_object);
8434
8435        let middle_point_id = *sketch.segments.get(1).unwrap();
8436
8437        let (src_delta, _scene_delta) = frontend
8438            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8439            .await
8440            .unwrap();
8441        // The line comment on the line above the deleted point must be preserved.
8442        // It is reattached to the next surviving body item.
8443        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8444
8445        ctx.close().await;
8446        mock_ctx.close().await;
8447    }
8448
8449    #[tokio::test(flavor = "multi_thread")]
8450    async fn test_delete_last_segment_preserves_pre_comment() {
8451        let initial_source = "\
8452sketch(on = XY) {
8453  point(at = [var 1, var 2])
8454  // describe the trailing point
8455  point(at = [var 3, var 4])
8456}
8457";
8458
8459        let program = Program::parse(initial_source).unwrap().0.unwrap();
8460        let mut frontend = FrontendState::new();
8461
8462        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8463        let mock_ctx = ExecutorContext::new_mock(None).await;
8464        let version = Version(0);
8465
8466        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8468        let sketch_id = sketch_object.id;
8469        let sketch = expect_sketch(sketch_object);
8470
8471        let last_point_id = *sketch.segments.last().unwrap();
8472
8473        let (src_delta, _scene_delta) = frontend
8474            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8475            .await
8476            .unwrap();
8477        // No following item to attach to; the comment is kept inside the sketch
8478        // block as trailing non-code metadata so the user does not lose it.
8479        insta::assert_snapshot!(
8480            "test_delete_last_segment_preserves_pre_comment",
8481            src_delta.text.as_str()
8482        );
8483
8484        ctx.close().await;
8485        mock_ctx.close().await;
8486    }
8487
8488    #[tokio::test(flavor = "multi_thread")]
8489    async fn test_delete_segment_drops_inline_trailing_comment() {
8490        let initial_source = "\
8491sketch(on = XY) {
8492  point(at = [var 1, var 2])
8493  point(at = [var 3, var 4]) // same-line note that gets dropped
8494  point(at = [var 5, var 6])
8495}
8496";
8497
8498        let program = Program::parse(initial_source).unwrap().0.unwrap();
8499        let mut frontend = FrontendState::new();
8500
8501        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8502        let mock_ctx = ExecutorContext::new_mock(None).await;
8503        let version = Version(0);
8504
8505        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8506        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8507        let sketch_id = sketch_object.id;
8508        let sketch = expect_sketch(sketch_object);
8509
8510        let middle_point_id = *sketch.segments.get(1).unwrap();
8511
8512        let (src_delta, _scene_delta) = frontend
8513            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8514            .await
8515            .unwrap();
8516        // The same-line trailing comment is removed along with the deleted code.
8517        assert!(
8518            !src_delta.text.contains("same-line note"),
8519            "inline comment should have been removed: {}",
8520            src_delta.text
8521        );
8522
8523        ctx.close().await;
8524        mock_ctx.close().await;
8525    }
8526
8527    #[tokio::test(flavor = "multi_thread")]
8528    async fn test_delete_segments_preserves_block_comments_across_positions() {
8529        // One test exercising several `delete_body_item_preserving_pre_comments`
8530        // branches at once with `/* ... */` block comments:
8531        //   - first point: leading block comment must migrate to the next item.
8532        //   - first point: same-line trailing block comment must be dropped.
8533        //   - middle point: leading block comment must stay attached after migration.
8534        //   - last point: leading block comment, with no surviving next item,
8535        //     must be converted into a trailing NonCodeNode.
8536        let initial_source = "\
8537sketch(on = XY) {
8538  /* above first - moves to middle */
8539  point(at = [var 1, var 2]) /* same-line on first - dropped */
8540  /* above middle - stays */
8541  point(at = [var 3, var 4])
8542  /* above last - moves to trailing meta */
8543  point(at = [var 5, var 6])
8544}
8545";
8546
8547        let program = Program::parse(initial_source).unwrap().0.unwrap();
8548        let mut frontend = FrontendState::new();
8549
8550        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8551        let mock_ctx = ExecutorContext::new_mock(None).await;
8552        let version = Version(0);
8553
8554        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8555        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8556        let sketch_id = sketch_object.id;
8557        let sketch = expect_sketch(sketch_object);
8558
8559        let first_point_id = *sketch.segments.first().unwrap();
8560        let last_point_id = *sketch.segments.last().unwrap();
8561
8562        let (src_delta, _scene_delta) = frontend
8563            .delete_objects(
8564                &mock_ctx,
8565                version,
8566                sketch_id,
8567                Vec::new(),
8568                vec![first_point_id, last_point_id],
8569            )
8570            .await
8571            .unwrap();
8572        insta::assert_snapshot!(
8573            "test_delete_segments_preserves_block_comments_across_positions",
8574            src_delta.text.as_str()
8575        );
8576
8577        ctx.close().await;
8578        mock_ctx.close().await;
8579    }
8580
8581    #[tokio::test(flavor = "multi_thread")]
8582    async fn test_edit_line_when_editing_its_start_point() {
8583        let initial_source = "\
8584sketch(on = XY) {
8585  line(start = [var 1, var 2], end = [var 3, var 4])
8586}
8587";
8588
8589        let program = Program::parse(initial_source).unwrap().0.unwrap();
8590
8591        let mut frontend = FrontendState::new();
8592
8593        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8594        let mock_ctx = ExecutorContext::new_mock(None).await;
8595        let version = Version(0);
8596
8597        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8598        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8599        let sketch_id = sketch_object.id;
8600        let sketch = expect_sketch(sketch_object);
8601
8602        let point_id = *sketch.segments.first().unwrap();
8603
8604        let point_ctor = PointCtor {
8605            position: Point2d {
8606                x: Expr::Var(Number {
8607                    value: 5.0,
8608                    units: NumericSuffix::Inch,
8609                }),
8610                y: Expr::Var(Number {
8611                    value: 6.0,
8612                    units: NumericSuffix::Inch,
8613                }),
8614            },
8615        };
8616        let segments = vec![ExistingSegmentCtor {
8617            id: point_id,
8618            ctor: SegmentCtor::Point(point_ctor),
8619        }];
8620        let (src_delta, scene_delta) = frontend
8621            .edit_segments(&mock_ctx, version, sketch_id, segments)
8622            .await
8623            .unwrap();
8624        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8625        assert_eq!(scene_delta.new_objects, vec![]);
8626        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628        ctx.close().await;
8629        mock_ctx.close().await;
8630    }
8631
8632    #[tokio::test(flavor = "multi_thread")]
8633    async fn test_edit_line_when_editing_its_end_point() {
8634        let initial_source = "\
8635sketch(on = XY) {
8636  line(start = [var 1, var 2], end = [var 3, var 4])
8637}
8638";
8639
8640        let program = Program::parse(initial_source).unwrap().0.unwrap();
8641
8642        let mut frontend = FrontendState::new();
8643
8644        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8645        let mock_ctx = ExecutorContext::new_mock(None).await;
8646        let version = Version(0);
8647
8648        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8649        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8650        let sketch_id = sketch_object.id;
8651        let sketch = expect_sketch(sketch_object);
8652        let point_id = *sketch.segments.get(1).unwrap();
8653
8654        let point_ctor = PointCtor {
8655            position: Point2d {
8656                x: Expr::Var(Number {
8657                    value: 5.0,
8658                    units: NumericSuffix::Inch,
8659                }),
8660                y: Expr::Var(Number {
8661                    value: 6.0,
8662                    units: NumericSuffix::Inch,
8663                }),
8664            },
8665        };
8666        let segments = vec![ExistingSegmentCtor {
8667            id: point_id,
8668            ctor: SegmentCtor::Point(point_ctor),
8669        }];
8670        let (src_delta, scene_delta) = frontend
8671            .edit_segments(&mock_ctx, version, sketch_id, segments)
8672            .await
8673            .unwrap();
8674        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
8675        assert_eq!(scene_delta.new_objects, vec![]);
8676        assert_eq!(
8677            scene_delta.new_graph.objects.len(),
8678            5,
8679            "{:#?}",
8680            scene_delta.new_graph.objects
8681        );
8682
8683        ctx.close().await;
8684        mock_ctx.close().await;
8685    }
8686
8687    #[tokio::test(flavor = "multi_thread")]
8688    async fn test_edit_line_with_coincident_feedback() {
8689        let initial_source = "\
8690sketch(on = XY) {
8691  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8692  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8693  fixed([line1.start, [0, 0]])
8694  coincident([line1.end, line2.start])
8695  equalLength([line1, line2])
8696}
8697";
8698
8699        let program = Program::parse(initial_source).unwrap().0.unwrap();
8700
8701        let mut frontend = FrontendState::new();
8702
8703        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8704        let mock_ctx = ExecutorContext::new_mock(None).await;
8705        let version = Version(0);
8706
8707        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8708        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8709        let sketch_id = sketch_object.id;
8710        let sketch = expect_sketch(sketch_object);
8711        let line2_end_id = *sketch.segments.get(4).unwrap();
8712
8713        let segments = vec![ExistingSegmentCtor {
8714            id: line2_end_id,
8715            ctor: SegmentCtor::Point(PointCtor {
8716                position: Point2d {
8717                    x: Expr::Var(Number {
8718                        value: 9.0,
8719                        units: NumericSuffix::None,
8720                    }),
8721                    y: Expr::Var(Number {
8722                        value: 10.0,
8723                        units: NumericSuffix::None,
8724                    }),
8725                },
8726            }),
8727        }];
8728        let (src_delta, scene_delta) = frontend
8729            .edit_segments(&mock_ctx, version, sketch_id, segments)
8730            .await
8731            .unwrap();
8732        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
8733        assert_eq!(
8734            scene_delta.new_graph.objects.len(),
8735            11,
8736            "{:#?}",
8737            scene_delta.new_graph.objects
8738        );
8739
8740        ctx.close().await;
8741        mock_ctx.close().await;
8742    }
8743
8744    #[tokio::test(flavor = "multi_thread")]
8745    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8746        let initial_source = "\
8747sketch(on = XY) {
8748  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8749  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8750  fixed([line1.start, [0, 0]])
8751  coincident([line1.end, line2.start])
8752  equalLength([line1, line2])
8753}
8754";
8755
8756        let program = Program::parse(initial_source).unwrap().0.unwrap();
8757        let mut frontend = FrontendState::new();
8758        let mock_ctx = ExecutorContext::new_mock(None).await;
8759        let version = Version(0);
8760
8761        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8762        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8763        let sketch_id = sketch_object.id;
8764        let sketch = expect_sketch(sketch_object);
8765        let line2_end_id = *sketch.segments.get(4).unwrap();
8766
8767        let segments = vec![ExistingSegmentCtor {
8768            id: line2_end_id,
8769            ctor: SegmentCtor::Point(PointCtor {
8770                position: Point2d {
8771                    x: Expr::Var(Number {
8772                        value: 9.0,
8773                        units: NumericSuffix::None,
8774                    }),
8775                    y: Expr::Var(Number {
8776                        value: 10.0,
8777                        units: NumericSuffix::None,
8778                    }),
8779                },
8780            }),
8781        }];
8782        let (edited_source, _) = frontend
8783            .edit_segments(&mock_ctx, version, sketch_id, segments)
8784            .await
8785            .unwrap();
8786
8787        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8788        assert_eq!(mock_source.text, edited_source.text);
8789
8790        mock_ctx.close().await;
8791    }
8792
8793    /// Preview segment edits should return solved geometry without persisting
8794    /// solver feedback to KCL.
8795    #[tokio::test(flavor = "multi_thread")]
8796    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8797        let initial_source = "\
8798sketch(on = XY) {
8799  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8800  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8801  fixed([line1.start, [0, 0]])
8802  coincident([line1.end, line2.start])
8803  equalLength([line1, line2])
8804}
8805";
8806
8807        let program = Program::parse(initial_source).unwrap().0.unwrap();
8808        let mut frontend = FrontendState::new();
8809        let mock_ctx = ExecutorContext::new_mock(None).await;
8810        let version = Version(0);
8811
8812        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8813        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8814        let sketch_id = sketch_object.id;
8815        let sketch = expect_sketch(sketch_object);
8816        let line2_end_id = *sketch.segments.get(4).unwrap();
8817
8818        let segments = vec![ExistingSegmentCtor {
8819            id: line2_end_id,
8820            ctor: SegmentCtor::Point(PointCtor {
8821                position: Point2d {
8822                    x: Expr::Var(Number {
8823                        value: 9.0,
8824                        units: NumericSuffix::None,
8825                    }),
8826                    y: Expr::Var(Number {
8827                        value: 10.0,
8828                        units: NumericSuffix::None,
8829                    }),
8830                },
8831            }),
8832        }];
8833        let (preview_source, preview_delta) = frontend
8834            .edit_segments_with_options(
8835                &mock_ctx,
8836                version,
8837                sketch_id,
8838                segments,
8839                EditSegmentsOptions {
8840                    anchor_segment_ids: Some(vec![line2_end_id]),
8841                    drag_anchors: Vec::new(),
8842                    commit_solved_initial_guesses: false,
8843                },
8844            )
8845            .await
8846            .unwrap();
8847
8848        assert!(
8849            !preview_delta.exec_outcome.var_solutions.is_empty(),
8850            "preview solve should still solve and return geometry feedback"
8851        );
8852        assert!(
8853            preview_source
8854                .text
8855                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8856        );
8857        assert!(
8858            preview_source
8859                .text
8860                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8861        );
8862
8863        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8864        assert_eq!(mock_source.text, preview_source.text);
8865
8866        mock_ctx.close().await;
8867    }
8868
8869    #[tokio::test(flavor = "multi_thread")]
8870    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8871        let initial_source = "\
8872sketch(on = XY) {
8873  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8874}
8875";
8876
8877        let program = Program::parse(initial_source).unwrap().0.unwrap();
8878        let mut frontend = FrontendState::new();
8879        let mock_ctx = ExecutorContext::new_mock(None).await;
8880        let version = Version(0);
8881
8882        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8883        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8884        let sketch_id = sketch_object.id;
8885        let sketch = expect_sketch(sketch_object);
8886        let line_end_id = *sketch.segments.get(1).unwrap();
8887
8888        let constraint = Constraint::Fixed(Fixed {
8889            points: vec![FixedPoint {
8890                point: line_end_id,
8891                position: Point2d {
8892                    x: Number {
8893                        value: 20.0,
8894                        units: NumericSuffix::Mm,
8895                    },
8896                    y: Number {
8897                        value: 0.0,
8898                        units: NumericSuffix::Mm,
8899                    },
8900                },
8901            }],
8902        });
8903        let (constraint_source, _) = frontend
8904            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8905            .await
8906            .unwrap();
8907
8908        assert!(
8909            constraint_source
8910                .text
8911                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8912            "{}",
8913            constraint_source.text
8914        );
8915        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8916        assert_eq!(mock_source.text, constraint_source.text);
8917
8918        mock_ctx.close().await;
8919    }
8920
8921    #[test]
8922    fn test_no_solver_feedback_preserves_original_source() {
8923        let initial_source = "\
8924@settings(defaultLengthUnit = in, kclVersion = 2.0)
8925cylinder = startSketchOn(XY)
8926    |> circle(center= [0, 0], radius= 22)
8927    |> extrude(length = 14)
8928";
8929        let mut frontend = FrontendState::new();
8930        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8931        let outcome = ExecOutcome {
8932            variables: Default::default(),
8933            operations: Default::default(),
8934            artifact_graph: Default::default(),
8935            scene_objects: Default::default(),
8936            source_range_to_object: Default::default(),
8937            var_solutions: Default::default(),
8938            refactor_metadata: Default::default(),
8939            issues: Default::default(),
8940            filenames: Default::default(),
8941            default_planes: Default::default(),
8942        };
8943
8944        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8945
8946        assert_eq!(source_delta.text, initial_source);
8947    }
8948
8949    /// Explicit drag anchors should limit which edited points become temporary
8950    /// fixed constraints.
8951    #[tokio::test(flavor = "multi_thread")]
8952    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8953        let initial_source = "\
8954sketch(on = XY) {
8955  point1 = point(at = [var 0mm, var 0mm])
8956  point2 = point(at = [var 0mm, var 0mm])
8957  coincident([point1, point2])
8958}
8959";
8960
8961        let program = Program::parse(initial_source).unwrap().0.unwrap();
8962        let mut frontend = FrontendState::new();
8963        let mock_ctx = ExecutorContext::new_mock(None).await;
8964        let version = Version(0);
8965
8966        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8967        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8968        let sketch_id = sketch_object.id;
8969        let sketch = expect_sketch(sketch_object);
8970        let point1_id = sketch.segments[0];
8971        let point2_id = sketch.segments[1];
8972
8973        let segments = vec![
8974            ExistingSegmentCtor {
8975                id: point1_id,
8976                ctor: SegmentCtor::Point(PointCtor {
8977                    position: point_expr_mm(10.0, 0.0),
8978                }),
8979            },
8980            ExistingSegmentCtor {
8981                id: point2_id,
8982                ctor: SegmentCtor::Point(PointCtor {
8983                    position: point_expr_mm(100.0, 0.0),
8984                }),
8985            },
8986        ];
8987        let (_, scene_delta) = frontend
8988            .edit_segments_with_options(
8989                &mock_ctx,
8990                version,
8991                sketch_id,
8992                segments,
8993                EditSegmentsOptions {
8994                    anchor_segment_ids: Some(vec![point1_id]),
8995                    drag_anchors: Vec::new(),
8996                    commit_solved_initial_guesses: true,
8997                },
8998            )
8999            .await
9000            .unwrap();
9001
9002        assert_point_position_close(
9003            point_position(&scene_delta.new_graph, point1_id),
9004            point_number_mm(10.0, 0.0),
9005        );
9006        assert_point_position_close(
9007            point_position(&scene_delta.new_graph, point2_id),
9008            point_number_mm(10.0, 0.0),
9009        );
9010
9011        mock_ctx.close().await;
9012    }
9013
9014    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9015    /// for every SketchVar whose initial NumericLiteral has the given value.
9016    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9017        use std::cell::RefCell;
9018        struct Collector {
9019            target: f64,
9020            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9021        }
9022        impl<'a> crate::walk::Visitor<'a> for &Collector {
9023            type Error = crate::front::Error;
9024            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9025                if let crate::walk::Node::SketchVar(sketch_var) = node
9026                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9027                    && (initial.value - self.target).abs() < 1e-9
9028                {
9029                    self.out
9030                        .borrow_mut()
9031                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9032                }
9033                for child in node.children().iter() {
9034                    if !child.visit(*self)? {
9035                        return Ok(false);
9036                    }
9037                }
9038                Ok(true)
9039            }
9040        }
9041        let collector = Collector {
9042            target: value,
9043            out: Default::default(),
9044        };
9045        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9046        collector.out.into_inner()
9047    }
9048
9049    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9050    /// for every SketchVar (including bare `var`).
9051    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9052        use std::cell::RefCell;
9053        struct Collector {
9054            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9055        }
9056        impl<'a> crate::walk::Visitor<'a> for &Collector {
9057            type Error = crate::front::Error;
9058            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9059                if let crate::walk::Node::SketchVar(sketch_var) = node
9060                    && let Some(node_path) = &sketch_var.node_path
9061                {
9062                    self.out
9063                        .borrow_mut()
9064                        .push((SourceRange::from(sketch_var), node_path.clone()));
9065                }
9066                for child in node.children().iter() {
9067                    if !child.visit(*self)? {
9068                        return Ok(false);
9069                    }
9070                }
9071                Ok(true)
9072            }
9073        }
9074        let collector = Collector {
9075            out: Default::default(),
9076        };
9077        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9078        collector.out.into_inner()
9079    }
9080
9081    fn empty_exec_outcome_with_var_solutions(
9082        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9083    ) -> ExecOutcome {
9084        ExecOutcome {
9085            variables: Default::default(),
9086            operations: Default::default(),
9087            artifact_graph: Default::default(),
9088            scene_objects: Default::default(),
9089            source_range_to_object: Default::default(),
9090            var_solutions,
9091            refactor_metadata: Default::default(),
9092            issues: Default::default(),
9093            filenames: Default::default(),
9094            default_planes: Default::default(),
9095        }
9096    }
9097
9098    /// Happy path: commit a var solution to a `var N` inside a sketch block
9099    /// using a correct NodePath. Confirms the node-path code path produces the
9100    /// expected source mutation.
9101    #[test]
9102    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9103        let initial_source = "\
9104sketch(on = XY) {
9105  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9106}
9107";
9108        let program = Program::parse(initial_source).unwrap().0.unwrap();
9109        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9110        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9111        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9112
9113        let mut frontend = FrontendState::new();
9114        frontend.program = program;
9115
9116        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9117            literal_range,
9118            Some(node_path),
9119            Number {
9120                value: 25.0,
9121                units: NumericSuffix::Mm,
9122            },
9123        )]);
9124
9125        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9126
9127        insta::assert_snapshot!(
9128            "test_commit_var_solution_by_node_path_updates_sketch_var",
9129            source_delta.text
9130        );
9131    }
9132
9133    /// Whitespace inserted earlier in the source shifts the original SketchVar
9134    /// SourceRange. With NodePath propagation the commit should still target
9135    /// the right `var`. We simulate this by collecting node_paths against a
9136    /// "compact" source, then loading the frontend with a "padded" source
9137    /// (whose byte offsets differ), and feeding the original (now stale)
9138    /// source range plus the correct node_path back into the commit.
9139    #[test]
9140    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9141        let compact_source = "\
9142sketch(on = XY) {
9143  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9144}
9145";
9146        let padded_source = "\
9147// added comment\n// added comment\n\nsketch(on = XY) {
9148  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9149}
9150";
9151        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9152        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9153
9154        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9155            .into_iter()
9156            .next()
9157            .expect("expected `var 10mm` in compact source");
9158        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9159            .into_iter()
9160            .next()
9161            .expect("expected `var 10mm` in padded source");
9162
9163        assert_ne!(
9164            compact_match.0, padded_match.0,
9165            "byte offsets must differ for this test to be meaningful"
9166        );
9167        assert_eq!(
9168            compact_match.1, padded_match.1,
9169            "node paths must agree across whitespace; that's the whole point of NodePath",
9170        );
9171
9172        let mut frontend = FrontendState::new();
9173        frontend.program = padded_program;
9174
9175        // Stale source range from the compact source + correct node_path.
9176        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9177            compact_match.0,
9178            Some(compact_match.1),
9179            Number {
9180                value: 30.0,
9181                units: NumericSuffix::Mm,
9182            },
9183        )]);
9184
9185        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9186
9187        insta::assert_snapshot!(
9188            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9189            source_delta.text
9190        );
9191    }
9192
9193    /// When multiple `var` declarations exist and the stale source range
9194    /// happens to land on a *different* var, the node_path must take
9195    /// precedence and the right var gets updated.
9196    #[test]
9197    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9198        let initial_source = "\
9199sketch(on = XY) {
9200  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9201}
9202";
9203        let program = Program::parse(initial_source).unwrap().0.unwrap();
9204
9205        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9206            .into_iter()
9207            .next()
9208            .expect("expected `var 10mm`");
9209        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9210            .into_iter()
9211            .next()
9212            .expect("expected `var 20mm`");
9213
9214        let mut frontend = FrontendState::new();
9215        frontend.program = program;
9216
9217        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9218        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9219            var_20.0,
9220            Some(var_10.1),
9221            Number {
9222                value: 33.0,
9223                units: NumericSuffix::Mm,
9224            },
9225        )]);
9226
9227        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9228
9229        insta::assert_snapshot!(
9230            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9231            source_delta.text
9232        );
9233    }
9234
9235    /// Bare `var` (no initial literal) is only locatable via node_path. With
9236    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9237    /// solver solution should fill the initial value in. The
9238    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9239    /// is gated as an experimental feature; without it the re-parse of the
9240    /// recast source rejects bare `var` declarations.
9241    #[test]
9242    fn test_commit_var_solution_writes_back_into_bare_var() {
9243        let initial_source = "\
9244@settings(experimentalFeatures = allow, kclVersion = 2.0)
9245sketch(on = XY) {
9246  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9247}
9248";
9249        let program = Program::parse(initial_source).unwrap().0.unwrap();
9250
9251        // Pick the first bare `var`; collect_all_sketch_vars returns every
9252        // SketchVar, including bare ones.
9253        let bare = collect_all_sketch_vars(&program)
9254            .into_iter()
9255            .find(|(range, _)| {
9256                // The bare `var` is exactly the 3 characters "var".
9257                range.end() - range.start() == 3
9258            })
9259            .expect("expected at least one bare `var`");
9260
9261        let mut frontend = FrontendState::new();
9262        frontend.program = program;
9263
9264        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9265            bare.0,
9266            Some(bare.1),
9267            Number {
9268                value: 7.0,
9269                units: NumericSuffix::Mm,
9270            },
9271        )]);
9272
9273        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9274
9275        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9276        // written as an explicit suffix so the bare var commits with units.
9277        // The recast adds a blank line after the `@settings` annotation.
9278        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9279    }
9280
9281    #[tokio::test(flavor = "multi_thread")]
9282    async fn test_delete_point_without_var() {
9283        let initial_source = "\
9284sketch(on = XY) {
9285  point(at = [var 1, var 2])
9286  point(at = [var 3, var 4])
9287  point(at = [var 5, var 6])
9288}
9289";
9290
9291        let program = Program::parse(initial_source).unwrap().0.unwrap();
9292
9293        let mut frontend = FrontendState::new();
9294
9295        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9296        let mock_ctx = ExecutorContext::new_mock(None).await;
9297        let version = Version(0);
9298
9299        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9300        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9301        let sketch_id = sketch_object.id;
9302        let sketch = expect_sketch(sketch_object);
9303
9304        let point_id = *sketch.segments.get(1).unwrap();
9305
9306        let (src_delta, scene_delta) = frontend
9307            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9308            .await
9309            .unwrap();
9310        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9311        assert_eq!(scene_delta.new_objects, vec![]);
9312        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9313
9314        ctx.close().await;
9315        mock_ctx.close().await;
9316    }
9317
9318    #[tokio::test(flavor = "multi_thread")]
9319    async fn test_delete_point_with_var() {
9320        let initial_source = "\
9321sketch(on = XY) {
9322  point(at = [var 1, var 2])
9323  point1 = point(at = [var 3, var 4])
9324  point(at = [var 5, var 6])
9325}
9326";
9327
9328        let program = Program::parse(initial_source).unwrap().0.unwrap();
9329
9330        let mut frontend = FrontendState::new();
9331
9332        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9333        let mock_ctx = ExecutorContext::new_mock(None).await;
9334        let version = Version(0);
9335
9336        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9337        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9338        let sketch_id = sketch_object.id;
9339        let sketch = expect_sketch(sketch_object);
9340
9341        let point_id = *sketch.segments.get(1).unwrap();
9342
9343        let (src_delta, scene_delta) = frontend
9344            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9345            .await
9346            .unwrap();
9347        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9348        assert_eq!(scene_delta.new_objects, vec![]);
9349        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9350
9351        ctx.close().await;
9352        mock_ctx.close().await;
9353    }
9354
9355    #[tokio::test(flavor = "multi_thread")]
9356    async fn test_delete_multiple_points() {
9357        let initial_source = "\
9358sketch(on = XY) {
9359  point(at = [var 1, var 2])
9360  point1 = point(at = [var 3, var 4])
9361  point(at = [var 5, var 6])
9362}
9363";
9364
9365        let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367        let mut frontend = FrontendState::new();
9368
9369        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9370        let mock_ctx = ExecutorContext::new_mock(None).await;
9371        let version = Version(0);
9372
9373        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9374        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9375        let sketch_id = sketch_object.id;
9376
9377        let sketch = expect_sketch(sketch_object);
9378
9379        let point1_id = *sketch.segments.first().unwrap();
9380        let point2_id = *sketch.segments.get(1).unwrap();
9381
9382        let (src_delta, scene_delta) = frontend
9383            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9384            .await
9385            .unwrap();
9386        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9387        assert_eq!(scene_delta.new_objects, vec![]);
9388        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9389
9390        ctx.close().await;
9391        mock_ctx.close().await;
9392    }
9393
9394    #[tokio::test(flavor = "multi_thread")]
9395    async fn test_delete_coincident_constraint() {
9396        let initial_source = "\
9397sketch(on = XY) {
9398  point1 = point(at = [var 1, var 2])
9399  point2 = point(at = [var 3, var 4])
9400  coincident([point1, point2])
9401  point(at = [var 5, var 6])
9402}
9403";
9404
9405        let program = Program::parse(initial_source).unwrap().0.unwrap();
9406
9407        let mut frontend = FrontendState::new();
9408
9409        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9410        let mock_ctx = ExecutorContext::new_mock(None).await;
9411        let version = Version(0);
9412
9413        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9414        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9415        let sketch_id = sketch_object.id;
9416        let sketch = expect_sketch(sketch_object);
9417
9418        let coincident_id = *sketch.constraints.first().unwrap();
9419
9420        let (src_delta, scene_delta) = frontend
9421            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9422            .await
9423            .unwrap();
9424        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9425        assert_eq!(scene_delta.new_objects, vec![]);
9426        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9427
9428        ctx.close().await;
9429        mock_ctx.close().await;
9430    }
9431
9432    #[tokio::test(flavor = "multi_thread")]
9433    async fn test_delete_line_cascades_to_coincident_constraint() {
9434        let initial_source = "\
9435sketch(on = XY) {
9436  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9437  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9438  coincident([line1.end, line2.start])
9439}
9440";
9441
9442        let program = Program::parse(initial_source).unwrap().0.unwrap();
9443
9444        let mut frontend = FrontendState::new();
9445
9446        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9447        let mock_ctx = ExecutorContext::new_mock(None).await;
9448        let version = Version(0);
9449
9450        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9451        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9452        let sketch_id = sketch_object.id;
9453        let sketch = expect_sketch(sketch_object);
9454        let line_id = *sketch.segments.get(5).unwrap();
9455
9456        let (src_delta, scene_delta) = frontend
9457            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9458            .await
9459            .unwrap();
9460        insta::assert_snapshot!(
9461            "test_delete_line_cascades_to_coincident_constraint",
9462            src_delta.text.as_str()
9463        );
9464        assert_eq!(
9465            scene_delta.new_graph.objects.len(),
9466            5,
9467            "{:#?}",
9468            scene_delta.new_graph.objects
9469        );
9470
9471        ctx.close().await;
9472        mock_ctx.close().await;
9473    }
9474
9475    #[tokio::test(flavor = "multi_thread")]
9476    async fn test_delete_line_cascades_to_distance_constraint() {
9477        let initial_source = "\
9478sketch(on = XY) {
9479  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9480  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9481  distance([line1.end, line2.start]) == 10mm
9482}
9483";
9484
9485        let program = Program::parse(initial_source).unwrap().0.unwrap();
9486
9487        let mut frontend = FrontendState::new();
9488
9489        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9490        let mock_ctx = ExecutorContext::new_mock(None).await;
9491        let version = Version(0);
9492
9493        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9494        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9495        let sketch_id = sketch_object.id;
9496        let sketch = expect_sketch(sketch_object);
9497        let line_id = *sketch.segments.get(5).unwrap();
9498
9499        let (src_delta, scene_delta) = frontend
9500            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9501            .await
9502            .unwrap();
9503        insta::assert_snapshot!(
9504            "test_delete_line_cascades_to_distance_constraint",
9505            src_delta.text.as_str()
9506        );
9507        assert_eq!(
9508            scene_delta.new_graph.objects.len(),
9509            5,
9510            "{:#?}",
9511            scene_delta.new_graph.objects
9512        );
9513
9514        ctx.close().await;
9515        mock_ctx.close().await;
9516    }
9517
9518    #[tokio::test(flavor = "multi_thread")]
9519    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9520        let initial_source = "\
9521sketch(on = XY) {
9522  point1 = point(at = [var 1, var 2])
9523  point2 = point(at = [var 3, var 4])
9524  horizontalDistance([point1, point2]) == 10mm
9525}
9526";
9527
9528        let program = Program::parse(initial_source).unwrap().0.unwrap();
9529
9530        let mut frontend = FrontendState::new();
9531
9532        let mock_ctx = ExecutorContext::new_mock(None).await;
9533        let version = Version(0);
9534
9535        frontend.program = program.clone();
9536        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9537        frontend.update_state_after_exec(outcome, true);
9538        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9539        let sketch_id = sketch_object.id;
9540        let sketch = expect_sketch(sketch_object);
9541        let point2_id = *sketch.segments.get(1).unwrap();
9542
9543        let (src_delta, scene_delta) = frontend
9544            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9545            .await
9546            .unwrap();
9547        insta::assert_snapshot!(
9548            "test_delete_point_cascades_to_horizontal_distance_constraint",
9549            src_delta.text.as_str()
9550        );
9551        assert_eq!(
9552            scene_delta.new_graph.objects.len(),
9553            3,
9554            "{:#?}",
9555            scene_delta.new_graph.objects
9556        );
9557
9558        mock_ctx.close().await;
9559    }
9560
9561    #[tokio::test(flavor = "multi_thread")]
9562    async fn test_delete_line_cascades_to_fixed_constraint() {
9563        let initial_source = "\
9564sketch(on = XY) {
9565  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9566  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9567  fixed([line1.start, [0, 0]])
9568}
9569";
9570
9571        let program = Program::parse(initial_source).unwrap().0.unwrap();
9572
9573        let mut frontend = FrontendState::new();
9574
9575        let mock_ctx = ExecutorContext::new_mock(None).await;
9576        let version = Version(0);
9577
9578        frontend.program = program.clone();
9579        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9580        frontend.update_state_after_exec(outcome, true);
9581        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9582        let sketch_id = sketch_object.id;
9583        let sketch = expect_sketch(sketch_object);
9584        let line1_id = *sketch.segments.get(2).unwrap();
9585
9586        let (src_delta, scene_delta) = frontend
9587            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9588            .await
9589            .unwrap();
9590        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9591        assert_eq!(
9592            scene_delta.new_graph.objects.len(),
9593            5,
9594            "{:#?}",
9595            scene_delta.new_graph.objects
9596        );
9597
9598        mock_ctx.close().await;
9599    }
9600
9601    #[tokio::test(flavor = "multi_thread")]
9602    async fn test_delete_line_cascades_to_midpoint_constraint() {
9603        let initial_source = "\
9604sketch(on = XY) {
9605  point1 = point(at = [var 1, var 2])
9606  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9607  midpoint(line1, point = point1)
9608}
9609";
9610
9611        let program = Program::parse(initial_source).unwrap().0.unwrap();
9612
9613        let mut frontend = FrontendState::new();
9614
9615        let mock_ctx = ExecutorContext::new_mock(None).await;
9616        let version = Version(0);
9617
9618        frontend.program = program.clone();
9619        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9620        frontend.update_state_after_exec(outcome, true);
9621        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9622        let sketch_id = sketch_object.id;
9623        let sketch = expect_sketch(sketch_object);
9624        let line1_id = *sketch.segments.get(3).unwrap();
9625
9626        let (src_delta, scene_delta) = frontend
9627            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9628            .await
9629            .unwrap();
9630        insta::assert_snapshot!(
9631            "test_delete_line_cascades_to_midpoint_constraint",
9632            src_delta.text.as_str()
9633        );
9634        assert_eq!(
9635            scene_delta.new_graph.objects.len(),
9636            3,
9637            "{:#?}",
9638            scene_delta.new_graph.objects
9639        );
9640
9641        mock_ctx.close().await;
9642    }
9643
9644    #[tokio::test(flavor = "multi_thread")]
9645    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9646        let initial_source = "\
9647sketch(on = XY) {
9648  point1 = point(at = [var 1, var 2])
9649  point2 = point(at = [var 3, var 4])
9650  point3 = point(at = [var 5, var 6])
9651  coincident([point1, point2, point3])
9652}
9653";
9654
9655        let program = Program::parse(initial_source).unwrap().0.unwrap();
9656
9657        let mut frontend = FrontendState::new();
9658
9659        let mock_ctx = ExecutorContext::new_mock(None).await;
9660        let version = Version(0);
9661
9662        frontend.program = program.clone();
9663        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9664        frontend.update_state_after_exec(outcome, true);
9665        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9666        let sketch_id = sketch_object.id;
9667        let sketch = expect_sketch(sketch_object);
9668        let point3_id = *sketch.segments.get(2).unwrap();
9669
9670        let (src_delta, scene_delta) = frontend
9671            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9672            .await
9673            .unwrap();
9674        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9675        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9676        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9677        assert!(
9678            src_delta.text.contains("coincident([point1, point2])"),
9679            "{}",
9680            src_delta.text
9681        );
9682
9683        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9684        let sketch = expect_sketch(sketch_object);
9685        assert_eq!(sketch.segments.len(), 2);
9686        assert_eq!(sketch.constraints.len(), 1);
9687
9688        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9689        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9690            panic!("Expected constraint object");
9691        };
9692        let Constraint::Coincident(coincident) = constraint else {
9693            panic!("Expected coincident constraint");
9694        };
9695        assert_eq!(
9696            coincident.segments,
9697            sketch
9698                .segments
9699                .iter()
9700                .copied()
9701                .map(Into::into)
9702                .collect::<Vec<ConstraintSegment>>()
9703        );
9704
9705        mock_ctx.close().await;
9706    }
9707
9708    #[tokio::test(flavor = "multi_thread")]
9709    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9710        let initial_source = "\
9711sketch(on = XY) {
9712  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9713  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9714  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9715  equalLength([line1, line2, line3])
9716}
9717";
9718
9719        let program = Program::parse(initial_source).unwrap().0.unwrap();
9720
9721        let mut frontend = FrontendState::new();
9722
9723        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9724        let mock_ctx = ExecutorContext::new_mock(None).await;
9725        let version = Version(0);
9726
9727        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9728        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9729        let sketch_id = sketch_object.id;
9730        let sketch = expect_sketch(sketch_object);
9731        let line3_id = *sketch.segments.get(8).unwrap();
9732
9733        let (src_delta, scene_delta) = frontend
9734            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9735            .await
9736            .unwrap();
9737        insta::assert_snapshot!(
9738            "test_delete_line_preserves_multiline_equal_length_constraint",
9739            src_delta.text.as_str()
9740        );
9741
9742        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9743        let sketch = expect_sketch(sketch_object);
9744        assert_eq!(sketch.constraints.len(), 1);
9745
9746        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9747        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9748            panic!("Expected constraint object");
9749        };
9750        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9751            panic!("Expected lines equal length constraint");
9752        };
9753        assert_eq!(lines_equal_length.lines.len(), 2);
9754
9755        ctx.close().await;
9756        mock_ctx.close().await;
9757    }
9758
9759    #[tokio::test(flavor = "multi_thread")]
9760    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9761        let initial_source = "\
9762sketch(on = XY) {
9763  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9764  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9765  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9766  horizontal([line1.end, line2.start, line3.start])
9767}
9768";
9769
9770        let program = Program::parse(initial_source).unwrap().0.unwrap();
9771
9772        let mut frontend = FrontendState::new();
9773
9774        let mock_ctx = ExecutorContext::new_mock(None).await;
9775        let version = Version(0);
9776
9777        frontend.program = program.clone();
9778        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9779        frontend.update_state_after_exec(outcome, true);
9780        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9781        let sketch_id = sketch_object.id;
9782        let sketch = expect_sketch(sketch_object);
9783        let line1_id = *sketch.segments.get(2).unwrap();
9784
9785        let (src_delta, scene_delta) = frontend
9786            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9787            .await
9788            .unwrap();
9789        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9790        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9791        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9792        assert!(
9793            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9794            "{}",
9795            src_delta.text
9796        );
9797
9798        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9799        let sketch = expect_sketch(sketch_object);
9800        assert_eq!(sketch.constraints.len(), 1);
9801
9802        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9803        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9804            panic!("Expected constraint object");
9805        };
9806        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9807            panic!("Expected horizontal points constraint");
9808        };
9809        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9810        assert_eq!(*points, remaining_points);
9811
9812        mock_ctx.close().await;
9813    }
9814
9815    #[tokio::test(flavor = "multi_thread")]
9816    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9817        let initial_source = "\
9818sketch(on = XY) {
9819  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9821  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9822  vertical([line1.end, line2.start, line3.start])
9823}
9824";
9825
9826        let program = Program::parse(initial_source).unwrap().0.unwrap();
9827
9828        let mut frontend = FrontendState::new();
9829
9830        let mock_ctx = ExecutorContext::new_mock(None).await;
9831        let version = Version(0);
9832
9833        frontend.program = program.clone();
9834        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9835        frontend.update_state_after_exec(outcome, true);
9836        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837        let sketch_id = sketch_object.id;
9838        let sketch = expect_sketch(sketch_object);
9839        let line1_id = *sketch.segments.get(2).unwrap();
9840
9841        let (src_delta, scene_delta) = frontend
9842            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9843            .await
9844            .unwrap();
9845        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9846        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9847        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9848        assert!(
9849            src_delta.text.contains("vertical([line2.start, line3.start])"),
9850            "{}",
9851            src_delta.text
9852        );
9853
9854        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9855        let sketch = expect_sketch(sketch_object);
9856        assert_eq!(sketch.constraints.len(), 1);
9857
9858        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9859        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9860            panic!("Expected constraint object");
9861        };
9862        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9863            panic!("Expected vertical points constraint");
9864        };
9865        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9866        assert_eq!(*points, remaining_points);
9867
9868        mock_ctx.close().await;
9869    }
9870
9871    #[tokio::test(flavor = "multi_thread")]
9872    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9873        let initial_source = "\
9874sketch(on = XY) {
9875  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9876  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9877  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9878  coincident([line1.end, line2.start, line3.start])
9879}
9880";
9881
9882        let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884        let mut frontend = FrontendState::new();
9885
9886        let mock_ctx = ExecutorContext::new_mock(None).await;
9887        let version = Version(0);
9888
9889        frontend.program = program.clone();
9890        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9891        frontend.update_state_after_exec(outcome, true);
9892        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9893        let sketch_id = sketch_object.id;
9894        let sketch = expect_sketch(sketch_object);
9895        let line1_id = *sketch.segments.get(2).unwrap();
9896
9897        let (src_delta, scene_delta) = frontend
9898            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9899            .await
9900            .unwrap();
9901        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9902        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9903        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9904        assert!(
9905            src_delta.text.contains("coincident([line2.start, line3.start])"),
9906            "{}",
9907            src_delta.text
9908        );
9909
9910        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9911        let sketch = expect_sketch(sketch_object);
9912        assert_eq!(sketch.constraints.len(), 1);
9913
9914        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9915        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9916            panic!("Expected constraint object");
9917        };
9918        let Constraint::Coincident(coincident) = constraint else {
9919            panic!("Expected coincident constraint");
9920        };
9921        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9922        assert_eq!(coincident.segments, remaining_segments);
9923
9924        mock_ctx.close().await;
9925    }
9926
9927    #[tokio::test(flavor = "multi_thread")]
9928    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9929        let initial_source = "\
9930sketch(on = XY) {
9931  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9932  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9933  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9934  equalLength([line1, line2, line3])
9935}
9936";
9937
9938        let program = Program::parse(initial_source).unwrap().0.unwrap();
9939
9940        let mut frontend = FrontendState::new();
9941
9942        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9943        let mock_ctx = ExecutorContext::new_mock(None).await;
9944        let version = Version(0);
9945
9946        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9947        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9948        let sketch_id = sketch_object.id;
9949        let sketch = expect_sketch(sketch_object);
9950        let line2_id = *sketch.segments.get(5).unwrap();
9951        let line3_id = *sketch.segments.get(8).unwrap();
9952
9953        let (src_delta, scene_delta) = frontend
9954            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9955            .await
9956            .unwrap();
9957        insta::assert_snapshot!(
9958            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
9959            src_delta.text.as_str()
9960        );
9961
9962        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9963        let sketch = expect_sketch(sketch_object);
9964        assert!(sketch.constraints.is_empty());
9965
9966        ctx.close().await;
9967        mock_ctx.close().await;
9968    }
9969
9970    #[tokio::test(flavor = "multi_thread")]
9971    async fn test_delete_line_preserves_multiline_parallel_constraint() {
9972        let initial_source = "\
9973sketch(on = XY) {
9974  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9975  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9976  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9977  parallel([line1, line2, line3])
9978}
9979";
9980
9981        let program = Program::parse(initial_source).unwrap().0.unwrap();
9982
9983        let mut frontend = FrontendState::new();
9984
9985        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9986        let mock_ctx = ExecutorContext::new_mock(None).await;
9987        let version = Version(0);
9988
9989        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9990        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9991        let sketch_id = sketch_object.id;
9992        let sketch = expect_sketch(sketch_object);
9993        let line3_id = *sketch.segments.get(8).unwrap();
9994
9995        let (src_delta, scene_delta) = frontend
9996            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9997            .await
9998            .unwrap();
9999        insta::assert_snapshot!(
10000            "test_delete_line_preserves_multiline_parallel_constraint",
10001            src_delta.text.as_str()
10002        );
10003
10004        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10005        let sketch = expect_sketch(sketch_object);
10006        assert_eq!(sketch.constraints.len(), 1);
10007
10008        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10009        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10010            panic!("Expected constraint object");
10011        };
10012        let Constraint::Parallel(parallel) = constraint else {
10013            panic!("Expected parallel constraint");
10014        };
10015        assert_eq!(parallel.lines.len(), 2);
10016
10017        ctx.close().await;
10018        mock_ctx.close().await;
10019    }
10020
10021    #[tokio::test(flavor = "multi_thread")]
10022    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10023        let initial_source = "\
10024sketch(on = XY) {
10025  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10026  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10027  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10028  parallel([line1, line2, line3])
10029}
10030";
10031
10032        let program = Program::parse(initial_source).unwrap().0.unwrap();
10033
10034        let mut frontend = FrontendState::new();
10035
10036        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10037        let mock_ctx = ExecutorContext::new_mock(None).await;
10038        let version = Version(0);
10039
10040        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10041        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10042        let sketch_id = sketch_object.id;
10043        let sketch = expect_sketch(sketch_object);
10044        let line2_id = *sketch.segments.get(5).unwrap();
10045        let line3_id = *sketch.segments.get(8).unwrap();
10046
10047        let (src_delta, scene_delta) = frontend
10048            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10049            .await
10050            .unwrap();
10051        insta::assert_snapshot!(
10052            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10053            src_delta.text.as_str()
10054        );
10055
10056        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10057        let sketch = expect_sketch(sketch_object);
10058        assert!(sketch.constraints.is_empty());
10059
10060        ctx.close().await;
10061        mock_ctx.close().await;
10062    }
10063
10064    #[tokio::test(flavor = "multi_thread")]
10065    async fn test_delete_line_line_coincident_constraint() {
10066        let initial_source = "\
10067sketch(on = XY) {
10068  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10069  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10070  coincident([line1, line2])
10071}
10072";
10073
10074        let program = Program::parse(initial_source).unwrap().0.unwrap();
10075
10076        let mut frontend = FrontendState::new();
10077
10078        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10079        let mock_ctx = ExecutorContext::new_mock(None).await;
10080        let version = Version(0);
10081
10082        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10083        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10084        let sketch_id = sketch_object.id;
10085        let sketch = expect_sketch(sketch_object);
10086
10087        let coincident_id = *sketch.constraints.first().unwrap();
10088
10089        let (src_delta, scene_delta) = frontend
10090            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10091            .await
10092            .unwrap();
10093        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10094        assert_eq!(scene_delta.new_objects, vec![]);
10095        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10096
10097        ctx.close().await;
10098        mock_ctx.close().await;
10099    }
10100
10101    #[tokio::test(flavor = "multi_thread")]
10102    async fn test_two_points_coincident() {
10103        let initial_source = "\
10104sketch(on = XY) {
10105  point1 = point(at = [var 1, var 2])
10106  point(at = [3, 4])
10107}
10108";
10109
10110        let program = Program::parse(initial_source).unwrap().0.unwrap();
10111
10112        let mut frontend = FrontendState::new();
10113
10114        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10115        let mock_ctx = ExecutorContext::new_mock(None).await;
10116        let version = Version(0);
10117
10118        frontend.hack_set_program(&ctx, program).await.unwrap();
10119        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10120        let sketch_id = sketch_object.id;
10121        let sketch = expect_sketch(sketch_object);
10122        let point0_id = *sketch.segments.first().unwrap();
10123        let point1_id = *sketch.segments.get(1).unwrap();
10124
10125        let constraint = Constraint::Coincident(Coincident {
10126            segments: vec![point0_id.into(), point1_id.into()],
10127        });
10128        let (src_delta, scene_delta) = frontend
10129            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10130            .await
10131            .unwrap();
10132        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10133        assert_eq!(
10134            scene_delta.new_graph.objects.len(),
10135            5,
10136            "{:#?}",
10137            scene_delta.new_graph.objects
10138        );
10139
10140        ctx.close().await;
10141        mock_ctx.close().await;
10142    }
10143
10144    #[tokio::test(flavor = "multi_thread")]
10145    async fn test_three_points_coincident() {
10146        let initial_source = "\
10147sketch(on = XY) {
10148  point1 = point(at = [var 1, var 2])
10149  point(at = [var 3, var 4])
10150  point(at = [var 5, var 6])
10151}
10152";
10153
10154        let program = Program::parse(initial_source).unwrap().0.unwrap();
10155
10156        let mut frontend = FrontendState::new();
10157
10158        let mock_ctx = ExecutorContext::new_mock(None).await;
10159        let version = Version(0);
10160
10161        frontend.program = program.clone();
10162        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10163        frontend.update_state_after_exec(outcome, true);
10164        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10165        let sketch_id = sketch_object.id;
10166        let sketch = expect_sketch(sketch_object);
10167        let segments = sketch
10168            .segments
10169            .iter()
10170            .take(3)
10171            .copied()
10172            .map(Into::into)
10173            .collect::<Vec<ConstraintSegment>>();
10174
10175        let constraint = Constraint::Coincident(Coincident {
10176            segments: segments.clone(),
10177        });
10178        let (src_delta, scene_delta) = frontend
10179            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10180            .await
10181            .unwrap();
10182        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10183
10184        let constraint_object = scene_delta
10185            .new_graph
10186            .objects
10187            .iter()
10188            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10189            .unwrap();
10190
10191        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10192            panic!("expected a constraint object");
10193        };
10194
10195        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10196
10197        mock_ctx.close().await;
10198    }
10199
10200    #[tokio::test(flavor = "multi_thread")]
10201    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10202        let initial_source = "\
10203sketch(on = XY) {
10204  point1 = point(at = [var 1, var 2])
10205  point2 = point(at = [var 3, var 4])
10206  point3 = point(at = [var 5, var 6])
10207  coincident([point1, point2, point3])
10208}
10209";
10210
10211        let program = Program::parse(initial_source).unwrap().0.unwrap();
10212
10213        let mut frontend = FrontendState::new();
10214
10215        let ctx = ExecutorContext::new_mock(None).await;
10216        frontend.program = program.clone();
10217        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10218        frontend.update_state_after_exec(outcome, true);
10219
10220        let constraint_object = frontend
10221            .scene_graph
10222            .objects
10223            .iter()
10224            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10225            .unwrap();
10226        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10227            panic!("expected a constraint object");
10228        };
10229
10230        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10231        let sketch = expect_sketch(sketch_object);
10232        let expected_segments = sketch
10233            .segments
10234            .iter()
10235            .take(3)
10236            .copied()
10237            .map(Into::into)
10238            .collect::<Vec<ConstraintSegment>>();
10239
10240        assert_eq!(
10241            constraint,
10242            &Constraint::Coincident(Coincident {
10243                segments: expected_segments,
10244            })
10245        );
10246
10247        ctx.close().await;
10248    }
10249
10250    #[tokio::test(flavor = "multi_thread")]
10251    async fn test_point_origin_coincident_preserves_order() {
10252        let initial_source = "\
10253sketch(on = XY) {
10254  point(at = [var 1, var 2])
10255}
10256";
10257
10258        for (origin_first, snapshot_name) in [
10259            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10260            (false, "test_point_origin_coincident_preserves_order_point_first"),
10261        ] {
10262            let program = Program::parse(initial_source).unwrap().0.unwrap();
10263
10264            let mut frontend = FrontendState::new();
10265
10266            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10267            let mock_ctx = ExecutorContext::new_mock(None).await;
10268            let version = Version(0);
10269
10270            frontend.hack_set_program(&ctx, program).await.unwrap();
10271            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10272            let sketch_id = sketch_object.id;
10273            let sketch = expect_sketch(sketch_object);
10274            let point_id = *sketch.segments.first().unwrap();
10275
10276            let segments = if origin_first {
10277                vec![ConstraintSegment::ORIGIN, point_id.into()]
10278            } else {
10279                vec![point_id.into(), ConstraintSegment::ORIGIN]
10280            };
10281            let constraint = Constraint::Coincident(Coincident {
10282                segments: segments.clone(),
10283            });
10284            let (src_delta, scene_delta) = frontend
10285                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10286                .await
10287                .unwrap();
10288            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10289
10290            let constraint_object = scene_delta
10291                .new_graph
10292                .objects
10293                .iter()
10294                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10295                .unwrap();
10296
10297            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10298                panic!("expected a constraint object");
10299            };
10300
10301            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10302
10303            ctx.close().await;
10304            mock_ctx.close().await;
10305        }
10306    }
10307
10308    #[tokio::test(flavor = "multi_thread")]
10309    async fn test_coincident_of_line_end_points() {
10310        let initial_source = "\
10311sketch(on = XY) {
10312  line(start = [var 1, var 2], end = [var 3, var 4])
10313  line(start = [var 5, var 6], end = [var 7, var 8])
10314}
10315";
10316
10317        let program = Program::parse(initial_source).unwrap().0.unwrap();
10318
10319        let mut frontend = FrontendState::new();
10320
10321        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10322        let mock_ctx = ExecutorContext::new_mock(None).await;
10323        let version = Version(0);
10324
10325        frontend.hack_set_program(&ctx, program).await.unwrap();
10326        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10327        let sketch_id = sketch_object.id;
10328        let sketch = expect_sketch(sketch_object);
10329        let point0_id = *sketch.segments.get(1).unwrap();
10330        let point1_id = *sketch.segments.get(3).unwrap();
10331
10332        let constraint = Constraint::Coincident(Coincident {
10333            segments: vec![point0_id.into(), point1_id.into()],
10334        });
10335        let (src_delta, scene_delta) = frontend
10336            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10337            .await
10338            .unwrap();
10339        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10340        assert_eq!(
10341            scene_delta.new_graph.objects.len(),
10342            9,
10343            "{:#?}",
10344            scene_delta.new_graph.objects
10345        );
10346
10347        ctx.close().await;
10348        mock_ctx.close().await;
10349    }
10350
10351    #[tokio::test(flavor = "multi_thread")]
10352    async fn test_coincident_of_line_point_and_circle_segment() {
10353        let initial_source = "\
10354sketch(on = XY) {
10355  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10356  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10357}
10358";
10359        let program = Program::parse(initial_source).unwrap().0.unwrap();
10360        let mut frontend = FrontendState::new();
10361
10362        let mock_ctx = ExecutorContext::new_mock(None).await;
10363        let version = Version(0);
10364
10365        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10366        frontend.program = program;
10367        frontend.update_state_after_exec(outcome, true);
10368        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10369        let sketch_id = sketch_object.id;
10370        let sketch = expect_sketch(sketch_object);
10371
10372        let circle_id = sketch
10373            .segments
10374            .iter()
10375            .copied()
10376            .find(|seg_id| {
10377                matches!(
10378                    &frontend.scene_graph.objects[seg_id.0].kind,
10379                    ObjectKind::Segment {
10380                        segment: Segment::Circle(_)
10381                    }
10382                )
10383            })
10384            .expect("Expected a circle segment in sketch");
10385        let line_id = frontend
10386            .scene_graph
10387            .objects
10388            .iter()
10389            .find_map(|obj| match &obj.kind {
10390                ObjectKind::Segment {
10391                    segment: Segment::Line(line),
10392                } if line.owner.is_none() => Some(obj.id),
10393                _ => None,
10394            })
10395            .expect("Expected a standalone line segment in scene graph");
10396
10397        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10398            ObjectKind::Segment {
10399                segment: Segment::Line(line),
10400            } => line.start,
10401            _ => panic!("Expected line segment object"),
10402        };
10403
10404        let constraint = Constraint::Coincident(Coincident {
10405            segments: vec![line_start_point_id.into(), circle_id.into()],
10406        });
10407        let (src_delta, _scene_delta) = frontend
10408            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10409            .await
10410            .unwrap();
10411        insta::assert_snapshot!(
10412            "test_coincident_of_line_point_and_circle_segment",
10413            src_delta.text.as_str()
10414        );
10415
10416        mock_ctx.close().await;
10417    }
10418
10419    #[tokio::test(flavor = "multi_thread")]
10420    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10421        // Test that attempting an invalid coincident constraint (arc and line)
10422        // doesn't corrupt the state, allowing subsequent operations to work.
10423        // This test verifies the transactional fix in add_constraint that prevents
10424        // state corruption when invalid constraints are attempted.
10425        // Example: coincident constraint between an arc segment and a straight line segment
10426        // is geometrically invalid and should fail, but state should remain intact.
10427        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10428        let program = Program::empty();
10429
10430        let mut frontend = FrontendState::new();
10431        frontend.program = program;
10432
10433        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10434        let mock_ctx = ExecutorContext::new_mock(None).await;
10435        let version = Version(0);
10436
10437        let sketch_args = SketchCtor {
10438            on: Plane::Default(PlaneName::Xy),
10439        };
10440        let (_src_delta, _scene_delta, sketch_id) = frontend
10441            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10442            .await
10443            .unwrap();
10444
10445        // Add an arc segment
10446        let arc_ctor = ArcCtor {
10447            start: Point2d {
10448                x: Expr::Var(Number {
10449                    value: 0.0,
10450                    units: NumericSuffix::Mm,
10451                }),
10452                y: Expr::Var(Number {
10453                    value: 0.0,
10454                    units: NumericSuffix::Mm,
10455                }),
10456            },
10457            end: Point2d {
10458                x: Expr::Var(Number {
10459                    value: 10.0,
10460                    units: NumericSuffix::Mm,
10461                }),
10462                y: Expr::Var(Number {
10463                    value: 10.0,
10464                    units: NumericSuffix::Mm,
10465                }),
10466            },
10467            center: Point2d {
10468                x: Expr::Var(Number {
10469                    value: 10.0,
10470                    units: NumericSuffix::Mm,
10471                }),
10472                y: Expr::Var(Number {
10473                    value: 0.0,
10474                    units: NumericSuffix::Mm,
10475                }),
10476            },
10477            construction: None,
10478        };
10479        let (_src_delta, scene_delta) = frontend
10480            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10481            .await
10482            .unwrap();
10483        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10484        let arc_id = *scene_delta.new_objects.last().unwrap();
10485
10486        // Add a line segment
10487        let line_ctor = LineCtor {
10488            start: Point2d {
10489                x: Expr::Var(Number {
10490                    value: 20.0,
10491                    units: NumericSuffix::Mm,
10492                }),
10493                y: Expr::Var(Number {
10494                    value: 0.0,
10495                    units: NumericSuffix::Mm,
10496                }),
10497            },
10498            end: Point2d {
10499                x: Expr::Var(Number {
10500                    value: 30.0,
10501                    units: NumericSuffix::Mm,
10502                }),
10503                y: Expr::Var(Number {
10504                    value: 10.0,
10505                    units: NumericSuffix::Mm,
10506                }),
10507            },
10508            construction: None,
10509        };
10510        let (_src_delta, scene_delta) = frontend
10511            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10512            .await
10513            .unwrap();
10514        // The line is the last object in new_objects (after the 2 points: start, end)
10515        let line_id = *scene_delta.new_objects.last().unwrap();
10516
10517        // Attempt to add an invalid coincident constraint between arc and line
10518        // This should fail during execution, but state should remain intact
10519        let constraint = Constraint::Coincident(Coincident {
10520            segments: vec![arc_id.into(), line_id.into()],
10521        });
10522        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10523
10524        // The constraint addition should fail (invalid constraint)
10525        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10526
10527        // Verify state is not corrupted by checking that we can still access the scene graph
10528        // and that the original segments are still present with their source ranges
10529        let sketch_object_after =
10530            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10531        let sketch_after = expect_sketch(sketch_object_after);
10532
10533        // Verify both segments are still in the sketch
10534        assert!(
10535            sketch_after.segments.contains(&arc_id),
10536            "Arc segment should still exist after failed constraint"
10537        );
10538        assert!(
10539            sketch_after.segments.contains(&line_id),
10540            "Line segment should still exist after failed constraint"
10541        );
10542
10543        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10544        let arc_obj = frontend
10545            .scene_graph
10546            .objects
10547            .get(arc_id.0)
10548            .expect("Arc object should still be accessible");
10549        let line_obj = frontend
10550            .scene_graph
10551            .objects
10552            .get(line_id.0)
10553            .expect("Line object should still be accessible");
10554
10555        // Verify source ranges are still valid (not corrupted)
10556        // Just verify that the objects are still accessible and have the expected types
10557        match &arc_obj.kind {
10558            ObjectKind::Segment {
10559                segment: Segment::Arc(_),
10560            } => {}
10561            _ => panic!("Arc object should still be an arc segment"),
10562        }
10563        match &line_obj.kind {
10564            ObjectKind::Segment {
10565                segment: Segment::Line(_),
10566            } => {}
10567            _ => panic!("Line object should still be a line segment"),
10568        }
10569
10570        ctx.close().await;
10571        mock_ctx.close().await;
10572    }
10573
10574    #[tokio::test(flavor = "multi_thread")]
10575    async fn test_distance_two_points() {
10576        let initial_source = "\
10577sketch(on = XY) {
10578  point(at = [var 1, var 2])
10579  point(at = [var 3, var 4])
10580}
10581";
10582
10583        let program = Program::parse(initial_source).unwrap().0.unwrap();
10584
10585        let mut frontend = FrontendState::new();
10586
10587        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10588        let mock_ctx = ExecutorContext::new_mock(None).await;
10589        let version = Version(0);
10590
10591        frontend.hack_set_program(&ctx, program).await.unwrap();
10592        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10593        let sketch_id = sketch_object.id;
10594        let sketch = expect_sketch(sketch_object);
10595        let point0_id = *sketch.segments.first().unwrap();
10596        let point1_id = *sketch.segments.get(1).unwrap();
10597
10598        let constraint = Constraint::Distance(Distance {
10599            points: vec![point0_id.into(), point1_id.into()],
10600            distance: Number {
10601                value: 2.0,
10602                units: NumericSuffix::Mm,
10603            },
10604            label_position: None,
10605            source: Default::default(),
10606        });
10607        let (src_delta, scene_delta) = frontend
10608            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10609            .await
10610            .unwrap();
10611        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10612        assert_eq!(
10613            scene_delta.new_graph.objects.len(),
10614            5,
10615            "{:#?}",
10616            scene_delta.new_graph.objects
10617        );
10618
10619        ctx.close().await;
10620        mock_ctx.close().await;
10621    }
10622
10623    #[tokio::test(flavor = "multi_thread")]
10624    async fn test_distance_two_points_with_label() {
10625        let initial_source = "\
10626sketch(on = XY) {
10627  point(at = [var 1, var 2])
10628  point(at = [var 3, var 4])
10629}
10630";
10631
10632        let program = Program::parse(initial_source).unwrap().0.unwrap();
10633
10634        let mut frontend = FrontendState::new();
10635
10636        let mock_ctx = ExecutorContext::new_mock(None).await;
10637        let version = Version(0);
10638
10639        frontend.program = program.clone();
10640        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10641        frontend.update_state_after_exec(outcome, true);
10642        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10643        let sketch_id = sketch_object.id;
10644        let sketch = expect_sketch(sketch_object);
10645        let point0_id = *sketch.segments.first().unwrap();
10646        let point1_id = *sketch.segments.get(1).unwrap();
10647
10648        let label_position = Point2d {
10649            x: Number {
10650                value: 10.0,
10651                units: NumericSuffix::Mm,
10652            },
10653            y: Number {
10654                value: 11.0,
10655                units: NumericSuffix::Mm,
10656            },
10657        };
10658        let constraint = Constraint::Distance(Distance {
10659            points: vec![point0_id.into(), point1_id.into()],
10660            distance: Number {
10661                value: 2.0,
10662                units: NumericSuffix::Mm,
10663            },
10664            label_position: Some(label_position.clone()),
10665            source: Default::default(),
10666        });
10667        let (src_delta, scene_delta) = frontend
10668            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10669            .await
10670            .unwrap();
10671        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
10672
10673        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10674        let sketch = expect_sketch(sketch_object);
10675        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10676        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10677            panic!("Expected constraint object");
10678        };
10679        let Constraint::Distance(distance) = constraint else {
10680            panic!("Expected distance constraint");
10681        };
10682        assert_eq!(distance.label_position, Some(label_position));
10683
10684        mock_ctx.close().await;
10685    }
10686
10687    #[tokio::test(flavor = "multi_thread")]
10688    async fn test_edit_distance_constraint_label_position() {
10689        let initial_source = "\
10690sketch(on = XY) {
10691  point(at = [var 1, var 2])
10692  point(at = [var 3, var 2])
10693}
10694";
10695
10696        let program = Program::parse(initial_source).unwrap().0.unwrap();
10697
10698        let mut frontend = FrontendState::new();
10699
10700        let mock_ctx = ExecutorContext::new_mock(None).await;
10701        let version = Version(0);
10702
10703        frontend.program = program.clone();
10704        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10705        frontend.update_state_after_exec(outcome, true);
10706        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10707        let sketch_id = sketch_object.id;
10708        let sketch = expect_sketch(sketch_object);
10709        let point0_id = *sketch.segments.first().unwrap();
10710        let point1_id = *sketch.segments.get(1).unwrap();
10711
10712        let constraint = Constraint::Distance(Distance {
10713            points: vec![point0_id.into(), point1_id.into()],
10714            distance: Number {
10715                value: 2.0,
10716                units: NumericSuffix::Mm,
10717            },
10718            label_position: None,
10719            source: Default::default(),
10720        });
10721        let (_, scene_delta) = frontend
10722            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10723            .await
10724            .unwrap();
10725        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10726        let sketch = expect_sketch(sketch_object);
10727        let constraint_id = sketch.constraints[0];
10728        let label_position = Point2d {
10729            x: Number {
10730                value: 10.0,
10731                units: NumericSuffix::Mm,
10732            },
10733            y: Number {
10734                value: 11.0,
10735                units: NumericSuffix::Mm,
10736            },
10737        };
10738
10739        let (src_delta, scene_delta) = frontend
10740            .edit_distance_constraint_label_position(
10741                &mock_ctx,
10742                version,
10743                sketch_id,
10744                constraint_id,
10745                label_position.clone(),
10746                vec![],
10747            )
10748            .await
10749            .unwrap();
10750        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
10751
10752        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10753        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10754            panic!("Expected constraint object");
10755        };
10756        let Constraint::Distance(distance) = constraint else {
10757            panic!("Expected distance constraint");
10758        };
10759        assert_eq!(distance.label_position, Some(label_position));
10760
10761        mock_ctx.close().await;
10762    }
10763
10764    #[tokio::test(flavor = "multi_thread")]
10765    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10766        let initial_source = "\
10767sketch(on = XY) {
10768  point1 = point(at = [var 0mm, var 0mm])
10769  point2 = point(at = [var 10mm, var 0mm])
10770  distance([point1, point2]) == 5mm
10771}
10772";
10773
10774        let program = Program::parse(initial_source).unwrap().0.unwrap();
10775        let mut frontend = FrontendState::new();
10776        let mock_ctx = ExecutorContext::new_mock(None).await;
10777        let version = Version(0);
10778
10779        frontend.program = program.clone();
10780        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10781        frontend.update_state_after_exec(outcome, true);
10782        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10783        let sketch_id = sketch_object.id;
10784        let sketch = expect_sketch(sketch_object);
10785        let point0_id = sketch.segments[0];
10786        let point1_id = sketch.segments[1];
10787        let constraint_id = sketch.constraints[0];
10788
10789        let edited_segments = vec![ExistingSegmentCtor {
10790            id: point0_id,
10791            ctor: SegmentCtor::Point(PointCtor {
10792                position: Point2d {
10793                    x: Expr::Var(Number {
10794                        value: 2.0,
10795                        units: NumericSuffix::Mm,
10796                    }),
10797                    y: Expr::Var(Number {
10798                        value: 1.0,
10799                        units: NumericSuffix::Mm,
10800                    }),
10801                },
10802            }),
10803        }];
10804        let (_, scene_delta) = frontend
10805            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10806            .await
10807            .unwrap();
10808        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10809        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10810
10811        let label_position = Point2d {
10812            x: Number {
10813                value: 3.0,
10814                units: NumericSuffix::Mm,
10815            },
10816            y: Number {
10817                value: 4.0,
10818                units: NumericSuffix::Mm,
10819            },
10820        };
10821        let (_, scene_delta) = frontend
10822            .edit_distance_constraint_label_position(
10823                &mock_ctx,
10824                version,
10825                sketch_id,
10826                constraint_id,
10827                label_position,
10828                vec![point0_id],
10829            )
10830            .await
10831            .unwrap();
10832
10833        assert_point_position_close(
10834            point_position(&scene_delta.new_graph, point0_id),
10835            point0_after_segment_edit,
10836        );
10837        assert_point_position_close(
10838            point_position(&scene_delta.new_graph, point1_id),
10839            point1_after_segment_edit,
10840        );
10841
10842        mock_ctx.close().await;
10843    }
10844
10845    #[tokio::test(flavor = "multi_thread")]
10846    async fn test_distance_point_line() {
10847        let initial_source = "\
10848sketch(on = XY) {
10849  point(at = [var 0, var 5])
10850  line(start = [var 0, var 0], end = [var 10, var 0])
10851}
10852";
10853
10854        let program = Program::parse(initial_source).unwrap().0.unwrap();
10855
10856        let mut frontend = FrontendState::new();
10857
10858        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10859        let mock_ctx = ExecutorContext::new_mock(None).await;
10860        let version = Version(0);
10861
10862        frontend.hack_set_program(&ctx, program).await.unwrap();
10863        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10864        let sketch_id = sketch_object.id;
10865        let sketch = expect_sketch(sketch_object);
10866        let point_id = *sketch.segments.first().unwrap();
10867        let line_id = *sketch
10868            .segments
10869            .iter()
10870            .find(|segment_id| {
10871                matches!(
10872                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10873                    Some(ObjectKind::Segment {
10874                        segment: Segment::Line(_)
10875                    })
10876                )
10877            })
10878            .unwrap();
10879
10880        let label_position = Point2d {
10881            x: Number {
10882                value: 10.0,
10883                units: NumericSuffix::Mm,
10884            },
10885            y: Number {
10886                value: 11.0,
10887                units: NumericSuffix::Mm,
10888            },
10889        };
10890        let constraint = Constraint::Distance(Distance {
10891            points: vec![point_id.into(), line_id.into()],
10892            distance: Number {
10893                value: 5.0,
10894                units: NumericSuffix::Mm,
10895            },
10896            label_position: Some(label_position.clone()),
10897            source: Default::default(),
10898        });
10899        let (src_delta, scene_delta) = frontend
10900            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10901            .await
10902            .unwrap();
10903        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
10904        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10905        let sketch = expect_sketch(sketch_object);
10906        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10907        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10908            panic!("Expected constraint object");
10909        };
10910        let Constraint::Distance(distance) = constraint else {
10911            panic!("Expected distance constraint");
10912        };
10913        assert_eq!(distance.label_position, Some(label_position));
10914
10915        ctx.close().await;
10916        mock_ctx.close().await;
10917    }
10918
10919    #[tokio::test(flavor = "multi_thread")]
10920    async fn test_distance_point_arc() {
10921        let initial_source = "\
10922sketch(on = XY) {
10923  point(at = [var 0, var 8])
10924  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10925}
10926";
10927
10928        let program = Program::parse(initial_source).unwrap().0.unwrap();
10929
10930        let mut frontend = FrontendState::new();
10931
10932        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10933        let mock_ctx = ExecutorContext::new_mock(None).await;
10934        let version = Version(0);
10935
10936        frontend.hack_set_program(&ctx, program).await.unwrap();
10937        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10938        let sketch_id = sketch_object.id;
10939        let sketch = expect_sketch(sketch_object);
10940        let point_id = *sketch.segments.first().unwrap();
10941        let arc_id = *sketch
10942            .segments
10943            .iter()
10944            .find(|segment_id| {
10945                matches!(
10946                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10947                    Some(ObjectKind::Segment {
10948                        segment: Segment::Arc(_)
10949                    })
10950                )
10951            })
10952            .unwrap();
10953
10954        let constraint = Constraint::Distance(Distance {
10955            points: vec![point_id.into(), arc_id.into()],
10956            distance: Number {
10957                value: 3.0,
10958                units: NumericSuffix::Mm,
10959            },
10960            label_position: None,
10961            source: Default::default(),
10962        });
10963        let (src_delta, _scene_delta) = frontend
10964            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10965            .await
10966            .unwrap();
10967        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
10968
10969        ctx.close().await;
10970        mock_ctx.close().await;
10971    }
10972
10973    #[tokio::test(flavor = "multi_thread")]
10974    async fn test_distance_arc_origin() {
10975        let initial_source = "\
10976sketch001 = sketch(on = XY) {
10977  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10978}
10979";
10980
10981        let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983        let mut frontend = FrontendState::new();
10984
10985        let mock_ctx = ExecutorContext::new_mock(None).await;
10986        let version = Version(0);
10987
10988        frontend.program = program.clone();
10989        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10990        frontend.update_state_after_exec(outcome, true);
10991        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10992        let sketch_id = sketch_object.id;
10993        let sketch = expect_sketch(sketch_object);
10994        let arc_id = *sketch
10995            .segments
10996            .iter()
10997            .find(|segment_id| {
10998                matches!(
10999                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11000                    Some(ObjectKind::Segment {
11001                        segment: Segment::Arc(_)
11002                    })
11003                )
11004            })
11005            .unwrap();
11006
11007        let constraint = Constraint::Distance(Distance {
11008            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11009            distance: Number {
11010                value: 3.0,
11011                units: NumericSuffix::Mm,
11012            },
11013            label_position: None,
11014            source: Default::default(),
11015        });
11016        let (src_delta, _scene_delta) = frontend
11017            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11018            .await
11019            .unwrap();
11020        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11021
11022        mock_ctx.close().await;
11023    }
11024
11025    #[tokio::test(flavor = "multi_thread")]
11026    async fn test_distance_line_origin() {
11027        let initial_source = "\
11028sketch(on = XY) {
11029  line(start = [var 5, var 0], end = [var 5, var 10])
11030}
11031";
11032
11033        let program = Program::parse(initial_source).unwrap().0.unwrap();
11034
11035        let mut frontend = FrontendState::new();
11036
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 line_id = *sketch
11047            .segments
11048            .iter()
11049            .find(|segment_id| {
11050                matches!(
11051                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11052                    Some(ObjectKind::Segment {
11053                        segment: Segment::Line(_)
11054                    })
11055                )
11056            })
11057            .unwrap();
11058
11059        let constraint = Constraint::Distance(Distance {
11060            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11061            distance: Number {
11062                value: 5.0,
11063                units: NumericSuffix::Mm,
11064            },
11065            label_position: None,
11066            source: Default::default(),
11067        });
11068        let (src_delta, _scene_delta) = frontend
11069            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11070            .await
11071            .unwrap();
11072        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11073
11074        mock_ctx.close().await;
11075    }
11076
11077    #[tokio::test(flavor = "multi_thread")]
11078    async fn test_distance_line_circle() {
11079        let initial_source = "\
11080sketch(on = XY) {
11081  line(start = [var -10, var 8], end = [var 10, var 8])
11082  circle(start = [var 5, var 0], center = [var 0, var 0])
11083}
11084";
11085
11086        let program = Program::parse(initial_source).unwrap().0.unwrap();
11087
11088        let mut frontend = FrontendState::new();
11089
11090        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11091        let mock_ctx = ExecutorContext::new_mock(None).await;
11092        let version = Version(0);
11093
11094        frontend.hack_set_program(&ctx, program).await.unwrap();
11095        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11096        let sketch_id = sketch_object.id;
11097        let sketch = expect_sketch(sketch_object);
11098        let line_id = *sketch
11099            .segments
11100            .iter()
11101            .find(|segment_id| {
11102                matches!(
11103                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11104                    Some(ObjectKind::Segment {
11105                        segment: Segment::Line(_)
11106                    })
11107                )
11108            })
11109            .unwrap();
11110        let circle_id = *sketch
11111            .segments
11112            .iter()
11113            .find(|segment_id| {
11114                matches!(
11115                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11116                    Some(ObjectKind::Segment {
11117                        segment: Segment::Circle(_)
11118                    })
11119                )
11120            })
11121            .unwrap();
11122
11123        let constraint = Constraint::Distance(Distance {
11124            points: vec![line_id.into(), circle_id.into()],
11125            distance: Number {
11126                value: 3.0,
11127                units: NumericSuffix::Mm,
11128            },
11129            label_position: None,
11130            source: Default::default(),
11131        });
11132        let (src_delta, _scene_delta) = frontend
11133            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11134            .await
11135            .unwrap();
11136        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11137
11138        ctx.close().await;
11139        mock_ctx.close().await;
11140    }
11141
11142    #[tokio::test(flavor = "multi_thread")]
11143    async fn test_distance_circle_arc() {
11144        let initial_source = "\
11145sketch(on = XY) {
11146  circle(start = [var 5, var 0], center = [var 0, var 0])
11147  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11148}
11149";
11150
11151        let program = Program::parse(initial_source).unwrap().0.unwrap();
11152
11153        let mut frontend = FrontendState::new();
11154
11155        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11156        let mock_ctx = ExecutorContext::new_mock(None).await;
11157        let version = Version(0);
11158
11159        frontend.hack_set_program(&ctx, program).await.unwrap();
11160        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11161        let sketch_id = sketch_object.id;
11162        let sketch = expect_sketch(sketch_object);
11163        let circle_id = *sketch
11164            .segments
11165            .iter()
11166            .find(|segment_id| {
11167                matches!(
11168                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11169                    Some(ObjectKind::Segment {
11170                        segment: Segment::Circle(_)
11171                    })
11172                )
11173            })
11174            .unwrap();
11175        let arc_id = *sketch
11176            .segments
11177            .iter()
11178            .find(|segment_id| {
11179                matches!(
11180                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11181                    Some(ObjectKind::Segment {
11182                        segment: Segment::Arc(_)
11183                    })
11184                )
11185            })
11186            .unwrap();
11187
11188        let constraint = Constraint::Distance(Distance {
11189            points: vec![circle_id.into(), arc_id.into()],
11190            distance: Number {
11191                value: 3.0,
11192                units: NumericSuffix::Mm,
11193            },
11194            label_position: None,
11195            source: Default::default(),
11196        });
11197        let (src_delta, _scene_delta) = frontend
11198            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11199            .await
11200            .unwrap();
11201        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
11202
11203        ctx.close().await;
11204        mock_ctx.close().await;
11205    }
11206
11207    #[tokio::test(flavor = "multi_thread")]
11208    async fn test_distance_parallel_lines() {
11209        let initial_source = "\
11210sketch(on = XY) {
11211  line(start = [var 0, var 0], end = [var 10, var 0])
11212  line(start = [var 0, var 5], end = [var 10, var 5])
11213}
11214";
11215
11216        let program = Program::parse(initial_source).unwrap().0.unwrap();
11217
11218        let mut frontend = FrontendState::new();
11219
11220        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11221        let mock_ctx = ExecutorContext::new_mock(None).await;
11222        let version = Version(0);
11223
11224        frontend.hack_set_program(&ctx, program).await.unwrap();
11225        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11226        let sketch_id = sketch_object.id;
11227        let sketch = expect_sketch(sketch_object);
11228        let line_ids = sketch
11229            .segments
11230            .iter()
11231            .copied()
11232            .filter(|segment_id| {
11233                matches!(
11234                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11235                    Some(ObjectKind::Segment {
11236                        segment: Segment::Line(_)
11237                    })
11238                )
11239            })
11240            .collect::<Vec<_>>();
11241
11242        let constraint = Constraint::Distance(Distance {
11243            points: vec![line_ids[0].into(), line_ids[1].into()],
11244            distance: Number {
11245                value: 5.0,
11246                units: NumericSuffix::Mm,
11247            },
11248            label_position: None,
11249            source: Default::default(),
11250        });
11251        let (src_delta, _scene_delta) = frontend
11252            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11253            .await
11254            .unwrap();
11255        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
11256
11257        ctx.close().await;
11258        mock_ctx.close().await;
11259    }
11260
11261    #[tokio::test(flavor = "multi_thread")]
11262    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11263        let initial_source = "\
11264sketch(on = XY) {
11265  line(start = [var 0, var 0], end = [var 10, var 0])
11266  line(start = [var 0, var 0], end = [var 0, var 10])
11267}
11268";
11269
11270        let program = Program::parse(initial_source).unwrap().0.unwrap();
11271
11272        let mut frontend = FrontendState::new();
11273
11274        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11275        let mock_ctx = ExecutorContext::new_mock(None).await;
11276        let version = Version(0);
11277
11278        frontend.hack_set_program(&ctx, program).await.unwrap();
11279        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11280        let sketch_id = sketch_object.id;
11281        let sketch = expect_sketch(sketch_object);
11282        let line_ids = sketch
11283            .segments
11284            .iter()
11285            .copied()
11286            .filter(|segment_id| {
11287                matches!(
11288                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11289                    Some(ObjectKind::Segment {
11290                        segment: Segment::Line(_)
11291                    })
11292                )
11293            })
11294            .collect::<Vec<_>>();
11295
11296        let constraint = Constraint::Distance(Distance {
11297            points: vec![line_ids[0].into(), line_ids[1].into()],
11298            distance: Number {
11299                value: 5.0,
11300                units: NumericSuffix::Mm,
11301            },
11302            label_position: None,
11303            source: Default::default(),
11304        });
11305        let (src_delta, _scene_delta) = frontend
11306            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11307            .await
11308            .unwrap();
11309        insta::assert_snapshot!(
11310            "test_distance_non_parallel_lines_lowers_to_distance",
11311            src_delta.text.as_str()
11312        );
11313
11314        ctx.close().await;
11315        mock_ctx.close().await;
11316    }
11317
11318    #[tokio::test(flavor = "multi_thread")]
11319    async fn test_horizontal_distance_two_points() {
11320        let initial_source = "\
11321sketch(on = XY) {
11322  point(at = [var 1, var 2])
11323  point(at = [var 3, var 4])
11324}
11325";
11326
11327        let program = Program::parse(initial_source).unwrap().0.unwrap();
11328
11329        let mut frontend = FrontendState::new();
11330
11331        let mock_ctx = ExecutorContext::new_mock(None).await;
11332        let version = Version(0);
11333
11334        frontend.program = program.clone();
11335        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11336        frontend.update_state_after_exec(outcome, true);
11337        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11338        let sketch_id = sketch_object.id;
11339        let sketch = expect_sketch(sketch_object);
11340        let point0_id = *sketch.segments.first().unwrap();
11341        let point1_id = *sketch.segments.get(1).unwrap();
11342        let label_position = Point2d {
11343            x: Number {
11344                value: 10.0,
11345                units: NumericSuffix::Mm,
11346            },
11347            y: Number {
11348                value: 11.0,
11349                units: NumericSuffix::Mm,
11350            },
11351        };
11352
11353        let constraint = Constraint::HorizontalDistance(Distance {
11354            points: vec![point0_id.into(), point1_id.into()],
11355            distance: Number {
11356                value: 2.0,
11357                units: NumericSuffix::Mm,
11358            },
11359            label_position: Some(label_position.clone()),
11360            source: Default::default(),
11361        });
11362        let (src_delta, scene_delta) = frontend
11363            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11364            .await
11365            .unwrap();
11366        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
11367        assert_eq!(
11368            scene_delta.new_graph.objects.len(),
11369            5,
11370            "{:#?}",
11371            scene_delta.new_graph.objects
11372        );
11373        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11374        let sketch = expect_sketch(sketch_object);
11375        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11376        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11377            panic!("Expected constraint object");
11378        };
11379        let Constraint::HorizontalDistance(distance) = constraint else {
11380            panic!("Expected horizontal distance constraint");
11381        };
11382        assert_eq!(distance.label_position, Some(label_position));
11383
11384        mock_ctx.close().await;
11385    }
11386
11387    #[tokio::test(flavor = "multi_thread")]
11388    async fn test_radius_single_arc_segment() {
11389        let initial_source = "\
11390sketch(on = XY) {
11391  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11392}
11393";
11394
11395        let program = Program::parse(initial_source).unwrap().0.unwrap();
11396
11397        let mut frontend = FrontendState::new();
11398
11399        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11400        let mock_ctx = ExecutorContext::new_mock(None).await;
11401        let version = Version(0);
11402
11403        frontend.hack_set_program(&ctx, program).await.unwrap();
11404        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11405        let sketch_id = sketch_object.id;
11406        let sketch = expect_sketch(sketch_object);
11407        // Find the arc segment (not the points)
11408        let arc_id = sketch
11409            .segments
11410            .iter()
11411            .find(|&seg_id| {
11412                let obj = frontend.scene_graph.objects.get(seg_id.0);
11413                matches!(
11414                    obj.map(|o| &o.kind),
11415                    Some(ObjectKind::Segment {
11416                        segment: Segment::Arc(_)
11417                    })
11418                )
11419            })
11420            .unwrap();
11421
11422        let constraint = Constraint::Radius(Radius {
11423            arc: *arc_id,
11424            radius: Number {
11425                value: 5.0,
11426                units: NumericSuffix::Mm,
11427            },
11428            label_position: None,
11429            source: Default::default(),
11430        });
11431        let (src_delta, scene_delta) = frontend
11432            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11433            .await
11434            .unwrap();
11435        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
11436        assert_eq!(
11437            scene_delta.new_graph.objects.len(),
11438            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11439            "{:#?}",
11440            scene_delta.new_graph.objects
11441        );
11442
11443        ctx.close().await;
11444        mock_ctx.close().await;
11445    }
11446
11447    #[tokio::test(flavor = "multi_thread")]
11448    async fn test_radius_single_arc_segment_with_label_position() {
11449        let initial_source = "\
11450sketch(on = XY) {
11451  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11452}
11453";
11454
11455        let program = Program::parse(initial_source).unwrap().0.unwrap();
11456        let mut frontend = FrontendState::new();
11457        let mock_ctx = ExecutorContext::new_mock(None).await;
11458        let version = Version(0);
11459
11460        frontend.program = program.clone();
11461        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11462        frontend.update_state_after_exec(outcome, true);
11463        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11464        let sketch_id = sketch_object.id;
11465        let sketch = expect_sketch(sketch_object);
11466        let arc_id = sketch
11467            .segments
11468            .iter()
11469            .find(|&seg_id| {
11470                let obj = frontend.scene_graph.objects.get(seg_id.0);
11471                matches!(
11472                    obj.map(|o| &o.kind),
11473                    Some(ObjectKind::Segment {
11474                        segment: Segment::Arc(_)
11475                    })
11476                )
11477            })
11478            .unwrap();
11479
11480        let label_position = Point2d {
11481            x: Number {
11482                value: 10.0,
11483                units: NumericSuffix::Mm,
11484            },
11485            y: Number {
11486                value: 11.0,
11487                units: NumericSuffix::Mm,
11488            },
11489        };
11490        let constraint = Constraint::Radius(Radius {
11491            arc: *arc_id,
11492            radius: Number {
11493                value: 5.0,
11494                units: NumericSuffix::Mm,
11495            },
11496            label_position: Some(label_position.clone()),
11497            source: Default::default(),
11498        });
11499        let (src_delta, scene_delta) = frontend
11500            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11501            .await
11502            .unwrap();
11503        insta::assert_snapshot!(
11504            "test_radius_single_arc_segment_with_label_position",
11505            src_delta.text.as_str()
11506        );
11507
11508        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11509        let sketch = expect_sketch(sketch_object);
11510        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11511        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11512            panic!("Expected constraint object");
11513        };
11514        let Constraint::Radius(radius) = constraint else {
11515            panic!("Expected radius constraint");
11516        };
11517        assert_eq!(radius.label_position, Some(label_position));
11518
11519        mock_ctx.close().await;
11520    }
11521
11522    #[tokio::test(flavor = "multi_thread")]
11523    async fn test_edit_radius_constraint_label_position() {
11524        let initial_source = "\
11525sketch(on = XY) {
11526  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11527  radius(arc1) == 5mm
11528}
11529";
11530
11531        let program = Program::parse(initial_source).unwrap().0.unwrap();
11532        let mut frontend = FrontendState::new();
11533        let mock_ctx = ExecutorContext::new_mock(None).await;
11534        let version = Version(0);
11535
11536        frontend.program = program.clone();
11537        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11538        frontend.update_state_after_exec(outcome, true);
11539        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11540        let sketch_id = sketch_object.id;
11541        let sketch = expect_sketch(sketch_object);
11542        let constraint_id = sketch.constraints[0];
11543        let label_position = Point2d {
11544            x: Number {
11545                value: 10.0,
11546                units: NumericSuffix::Mm,
11547            },
11548            y: Number {
11549                value: 11.0,
11550                units: NumericSuffix::Mm,
11551            },
11552        };
11553
11554        let (src_delta, scene_delta) = frontend
11555            .edit_distance_constraint_label_position(
11556                &mock_ctx,
11557                version,
11558                sketch_id,
11559                constraint_id,
11560                label_position.clone(),
11561                vec![],
11562            )
11563            .await
11564            .unwrap();
11565        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
11566
11567        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11568        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11569            panic!("Expected constraint object");
11570        };
11571        let Constraint::Radius(radius) = constraint else {
11572            panic!("Expected radius constraint");
11573        };
11574        assert_eq!(radius.label_position, Some(label_position));
11575
11576        mock_ctx.close().await;
11577    }
11578
11579    #[tokio::test(flavor = "multi_thread")]
11580    async fn test_vertical_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::VerticalDistance(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_vertical_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::VerticalDistance(distance) = constraint else {
11641            panic!("Expected vertical 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_add_fixed_standalone_point() {
11650        let initial_source = "\
11651sketch(on = XY) {
11652  point(at = [var 1, var 2])
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        let point_id = *sketch.segments.first().unwrap();
11669
11670        let (src_delta, scene_delta) = frontend
11671            .add_constraint(
11672                &mock_ctx,
11673                version,
11674                sketch_id,
11675                Constraint::Fixed(Fixed {
11676                    points: vec![FixedPoint {
11677                        point: point_id,
11678                        position: Point2d {
11679                            x: Number {
11680                                value: 2.0,
11681                                units: NumericSuffix::Mm,
11682                            },
11683                            y: Number {
11684                                value: 3.0,
11685                                units: NumericSuffix::Mm,
11686                            },
11687                        },
11688                    }],
11689                }),
11690            )
11691            .await
11692            .unwrap();
11693        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
11694        assert_eq!(
11695            scene_delta.new_graph.objects.len(),
11696            4,
11697            "{:#?}",
11698            scene_delta.new_graph.objects
11699        );
11700
11701        ctx.close().await;
11702        mock_ctx.close().await;
11703    }
11704
11705    #[tokio::test(flavor = "multi_thread")]
11706    async fn test_add_fixed_multiple_points() {
11707        let initial_source = "\
11708sketch(on = XY) {
11709  point(at = [var 1, var 2])
11710  point(at = [var 3, var 4])
11711}
11712";
11713
11714        let program = Program::parse(initial_source).unwrap().0.unwrap();
11715
11716        let mut frontend = FrontendState::new();
11717
11718        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11719        let mock_ctx = ExecutorContext::new_mock(None).await;
11720        let version = Version(0);
11721
11722        frontend.hack_set_program(&ctx, program).await.unwrap();
11723        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11724        let sketch_id = sketch_object.id;
11725        let sketch = expect_sketch(sketch_object);
11726        let point0_id = *sketch.segments.first().unwrap();
11727        let point1_id = *sketch.segments.get(1).unwrap();
11728
11729        let (src_delta, scene_delta) = frontend
11730            .add_constraint(
11731                &mock_ctx,
11732                version,
11733                sketch_id,
11734                Constraint::Fixed(Fixed {
11735                    points: vec![
11736                        FixedPoint {
11737                            point: point0_id,
11738                            position: Point2d {
11739                                x: Number {
11740                                    value: 2.0,
11741                                    units: NumericSuffix::Mm,
11742                                },
11743                                y: Number {
11744                                    value: 3.0,
11745                                    units: NumericSuffix::Mm,
11746                                },
11747                            },
11748                        },
11749                        FixedPoint {
11750                            point: point1_id,
11751                            position: Point2d {
11752                                x: Number {
11753                                    value: 4.0,
11754                                    units: NumericSuffix::Mm,
11755                                },
11756                                y: Number {
11757                                    value: 5.0,
11758                                    units: NumericSuffix::Mm,
11759                                },
11760                            },
11761                        },
11762                    ],
11763                }),
11764            )
11765            .await
11766            .unwrap();
11767        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
11768        assert_eq!(
11769            scene_delta.new_graph.objects.len(),
11770            6,
11771            "{:#?}",
11772            scene_delta.new_graph.objects
11773        );
11774
11775        ctx.close().await;
11776        mock_ctx.close().await;
11777    }
11778
11779    #[tokio::test(flavor = "multi_thread")]
11780    async fn test_add_fixed_owned_point() {
11781        let initial_source = "\
11782sketch(on = XY) {
11783  line(start = [var 1, var 2], end = [var 3, var 4])
11784}
11785";
11786
11787        let program = Program::parse(initial_source).unwrap().0.unwrap();
11788
11789        let mut frontend = FrontendState::new();
11790
11791        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11792        let mock_ctx = ExecutorContext::new_mock(None).await;
11793        let version = Version(0);
11794
11795        frontend.hack_set_program(&ctx, program).await.unwrap();
11796        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11797        let sketch_id = sketch_object.id;
11798        let sketch = expect_sketch(sketch_object);
11799        let line_start_id = *sketch.segments.first().unwrap();
11800
11801        let (src_delta, scene_delta) = frontend
11802            .add_constraint(
11803                &mock_ctx,
11804                version,
11805                sketch_id,
11806                Constraint::Fixed(Fixed {
11807                    points: vec![FixedPoint {
11808                        point: line_start_id,
11809                        position: Point2d {
11810                            x: Number {
11811                                value: 2.0,
11812                                units: NumericSuffix::Mm,
11813                            },
11814                            y: Number {
11815                                value: 3.0,
11816                                units: NumericSuffix::Mm,
11817                            },
11818                        },
11819                    }],
11820                }),
11821            )
11822            .await
11823            .unwrap();
11824        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
11825        assert_eq!(
11826            scene_delta.new_graph.objects.len(),
11827            6,
11828            "{:#?}",
11829            scene_delta.new_graph.objects
11830        );
11831
11832        ctx.close().await;
11833        mock_ctx.close().await;
11834    }
11835
11836    #[tokio::test(flavor = "multi_thread")]
11837    async fn test_radius_error_cases() {
11838        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11839        let mock_ctx = ExecutorContext::new_mock(None).await;
11840        let version = Version(0);
11841
11842        // Test: Single point should error
11843        let initial_source_point = "\
11844sketch(on = XY) {
11845  point(at = [var 1, var 2])
11846}
11847";
11848        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11849        let mut frontend_point = FrontendState::new();
11850        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11851        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11852        let sketch_id_point = sketch_object_point.id;
11853        let sketch_point = expect_sketch(sketch_object_point);
11854        let point_id = *sketch_point.segments.first().unwrap();
11855
11856        let constraint_point = Constraint::Radius(Radius {
11857            arc: point_id,
11858            radius: Number {
11859                value: 5.0,
11860                units: NumericSuffix::Mm,
11861            },
11862            label_position: None,
11863            source: Default::default(),
11864        });
11865        let result_point = frontend_point
11866            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11867            .await;
11868        assert!(result_point.is_err(), "Single point should error for radius");
11869
11870        // Test: Single line segment should error (only arc segments supported)
11871        let initial_source_line = "\
11872sketch(on = XY) {
11873  line(start = [var 1, var 2], end = [var 3, var 4])
11874}
11875";
11876        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11877        let mut frontend_line = FrontendState::new();
11878        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11879        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11880        let sketch_id_line = sketch_object_line.id;
11881        let sketch_line = expect_sketch(sketch_object_line);
11882        let line_id = *sketch_line.segments.first().unwrap();
11883
11884        let constraint_line = Constraint::Radius(Radius {
11885            arc: line_id,
11886            radius: Number {
11887                value: 5.0,
11888                units: NumericSuffix::Mm,
11889            },
11890            label_position: None,
11891            source: Default::default(),
11892        });
11893        let result_line = frontend_line
11894            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11895            .await;
11896        assert!(result_line.is_err(), "Single line segment should error for radius");
11897
11898        ctx.close().await;
11899        mock_ctx.close().await;
11900    }
11901
11902    #[tokio::test(flavor = "multi_thread")]
11903    async fn test_diameter_single_arc_segment() {
11904        let initial_source = "\
11905sketch(on = XY) {
11906  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11907}
11908";
11909
11910        let program = Program::parse(initial_source).unwrap().0.unwrap();
11911
11912        let mut frontend = FrontendState::new();
11913
11914        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11915        let mock_ctx = ExecutorContext::new_mock(None).await;
11916        let version = Version(0);
11917
11918        frontend.hack_set_program(&ctx, program).await.unwrap();
11919        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11920        let sketch_id = sketch_object.id;
11921        let sketch = expect_sketch(sketch_object);
11922        // Find the arc segment (not the points)
11923        let arc_id = sketch
11924            .segments
11925            .iter()
11926            .find(|&seg_id| {
11927                let obj = frontend.scene_graph.objects.get(seg_id.0);
11928                matches!(
11929                    obj.map(|o| &o.kind),
11930                    Some(ObjectKind::Segment {
11931                        segment: Segment::Arc(_)
11932                    })
11933                )
11934            })
11935            .unwrap();
11936
11937        let constraint = Constraint::Diameter(Diameter {
11938            arc: *arc_id,
11939            diameter: Number {
11940                value: 10.0,
11941                units: NumericSuffix::Mm,
11942            },
11943            label_position: None,
11944            source: Default::default(),
11945        });
11946        let (src_delta, scene_delta) = frontend
11947            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11948            .await
11949            .unwrap();
11950        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
11951        assert_eq!(
11952            scene_delta.new_graph.objects.len(),
11953            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11954            "{:#?}",
11955            scene_delta.new_graph.objects
11956        );
11957
11958        ctx.close().await;
11959        mock_ctx.close().await;
11960    }
11961
11962    #[tokio::test(flavor = "multi_thread")]
11963    async fn test_diameter_single_arc_segment_with_label_position() {
11964        let initial_source = "\
11965sketch(on = XY) {
11966  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11967}
11968";
11969
11970        let program = Program::parse(initial_source).unwrap().0.unwrap();
11971        let mut frontend = FrontendState::new();
11972        let mock_ctx = ExecutorContext::new_mock(None).await;
11973        let version = Version(0);
11974
11975        frontend.program = program.clone();
11976        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11977        frontend.update_state_after_exec(outcome, true);
11978        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11979        let sketch_id = sketch_object.id;
11980        let sketch = expect_sketch(sketch_object);
11981        let arc_id = sketch
11982            .segments
11983            .iter()
11984            .find(|&seg_id| {
11985                let obj = frontend.scene_graph.objects.get(seg_id.0);
11986                matches!(
11987                    obj.map(|o| &o.kind),
11988                    Some(ObjectKind::Segment {
11989                        segment: Segment::Arc(_)
11990                    })
11991                )
11992            })
11993            .unwrap();
11994
11995        let label_position = Point2d {
11996            x: Number {
11997                value: 10.0,
11998                units: NumericSuffix::Mm,
11999            },
12000            y: Number {
12001                value: 11.0,
12002                units: NumericSuffix::Mm,
12003            },
12004        };
12005        let constraint = Constraint::Diameter(Diameter {
12006            arc: *arc_id,
12007            diameter: Number {
12008                value: 10.0,
12009                units: NumericSuffix::Mm,
12010            },
12011            label_position: Some(label_position.clone()),
12012            source: Default::default(),
12013        });
12014        let (src_delta, scene_delta) = frontend
12015            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12016            .await
12017            .unwrap();
12018        insta::assert_snapshot!(
12019            "test_diameter_single_arc_segment_with_label_position",
12020            src_delta.text.as_str()
12021        );
12022
12023        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12024        let sketch = expect_sketch(sketch_object);
12025        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12026        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12027            panic!("Expected constraint object");
12028        };
12029        let Constraint::Diameter(diameter) = constraint else {
12030            panic!("Expected diameter constraint");
12031        };
12032        assert_eq!(diameter.label_position, Some(label_position));
12033
12034        mock_ctx.close().await;
12035    }
12036
12037    #[tokio::test(flavor = "multi_thread")]
12038    async fn test_edit_diameter_constraint_label_position() {
12039        let initial_source = "\
12040sketch(on = XY) {
12041  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12042  diameter(arc1) == 10mm
12043}
12044";
12045
12046        let program = Program::parse(initial_source).unwrap().0.unwrap();
12047        let mut frontend = FrontendState::new();
12048        let mock_ctx = ExecutorContext::new_mock(None).await;
12049        let version = Version(0);
12050
12051        frontend.program = program.clone();
12052        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12053        frontend.update_state_after_exec(outcome, true);
12054        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12055        let sketch_id = sketch_object.id;
12056        let sketch = expect_sketch(sketch_object);
12057        let constraint_id = sketch.constraints[0];
12058        let label_position = Point2d {
12059            x: Number {
12060                value: 10.0,
12061                units: NumericSuffix::Mm,
12062            },
12063            y: Number {
12064                value: 11.0,
12065                units: NumericSuffix::Mm,
12066            },
12067        };
12068
12069        let (src_delta, scene_delta) = frontend
12070            .edit_distance_constraint_label_position(
12071                &mock_ctx,
12072                version,
12073                sketch_id,
12074                constraint_id,
12075                label_position.clone(),
12076                vec![],
12077            )
12078            .await
12079            .unwrap();
12080        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12081
12082        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12083        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12084            panic!("Expected constraint object");
12085        };
12086        let Constraint::Diameter(diameter) = constraint else {
12087            panic!("Expected diameter constraint");
12088        };
12089        assert_eq!(diameter.label_position, Some(label_position));
12090
12091        mock_ctx.close().await;
12092    }
12093
12094    #[tokio::test(flavor = "multi_thread")]
12095    async fn test_diameter_error_cases() {
12096        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12097        let mock_ctx = ExecutorContext::new_mock(None).await;
12098        let version = Version(0);
12099
12100        // Test: Single point should error
12101        let initial_source_point = "\
12102sketch(on = XY) {
12103  point(at = [var 1, var 2])
12104}
12105";
12106        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12107        let mut frontend_point = FrontendState::new();
12108        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12109        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12110        let sketch_id_point = sketch_object_point.id;
12111        let sketch_point = expect_sketch(sketch_object_point);
12112        let point_id = *sketch_point.segments.first().unwrap();
12113
12114        let constraint_point = Constraint::Diameter(Diameter {
12115            arc: point_id,
12116            diameter: Number {
12117                value: 10.0,
12118                units: NumericSuffix::Mm,
12119            },
12120            label_position: None,
12121            source: Default::default(),
12122        });
12123        let result_point = frontend_point
12124            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12125            .await;
12126        assert!(result_point.is_err(), "Single point should error for diameter");
12127
12128        // Test: Single line segment should error (only arc segments supported)
12129        let initial_source_line = "\
12130sketch(on = XY) {
12131  line(start = [var 1, var 2], end = [var 3, var 4])
12132}
12133";
12134        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12135        let mut frontend_line = FrontendState::new();
12136        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12137        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12138        let sketch_id_line = sketch_object_line.id;
12139        let sketch_line = expect_sketch(sketch_object_line);
12140        let line_id = *sketch_line.segments.first().unwrap();
12141
12142        let constraint_line = Constraint::Diameter(Diameter {
12143            arc: line_id,
12144            diameter: Number {
12145                value: 10.0,
12146                units: NumericSuffix::Mm,
12147            },
12148            label_position: None,
12149            source: Default::default(),
12150        });
12151        let result_line = frontend_line
12152            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12153            .await;
12154        assert!(result_line.is_err(), "Single line segment should error for diameter");
12155
12156        ctx.close().await;
12157        mock_ctx.close().await;
12158    }
12159
12160    #[tokio::test(flavor = "multi_thread")]
12161    async fn test_line_horizontal() {
12162        let initial_source = "\
12163sketch(on = XY) {
12164  line(start = [var 1, var 2], end = [var 3, var 4])
12165}
12166";
12167
12168        let program = Program::parse(initial_source).unwrap().0.unwrap();
12169
12170        let mut frontend = FrontendState::new();
12171
12172        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12173        let mock_ctx = ExecutorContext::new_mock(None).await;
12174        let version = Version(0);
12175
12176        frontend.hack_set_program(&ctx, program).await.unwrap();
12177        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12178        let sketch_id = sketch_object.id;
12179        let sketch = expect_sketch(sketch_object);
12180        let line1_id = *sketch.segments.get(2).unwrap();
12181
12182        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12183        let (src_delta, scene_delta) = frontend
12184            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12185            .await
12186            .unwrap();
12187        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
12188        assert_eq!(
12189            scene_delta.new_graph.objects.len(),
12190            6,
12191            "{:#?}",
12192            scene_delta.new_graph.objects
12193        );
12194
12195        ctx.close().await;
12196        mock_ctx.close().await;
12197    }
12198
12199    #[tokio::test(flavor = "multi_thread")]
12200    async fn test_control_point_spline_edge_horizontal() {
12201        let initial_source = "\
12202@settings(experimentalFeatures = allow)
12203splineSketch = sketch(on = XY) {
12204  controlPointSpline1 = controlPointSpline(points = [
12205    [var 0mm, var 0mm],
12206    [var 10mm, var 20mm],
12207    [var 20mm, var 0mm],
12208  ])
12209}
12210";
12211
12212        let program = Program::parse(initial_source).unwrap().0.unwrap();
12213
12214        let mut frontend = FrontendState::new();
12215
12216        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12217        let mock_ctx = ExecutorContext::new_mock(None).await;
12218        let version = Version(0);
12219
12220        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12221        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12222        let sketch_id = sketch_object.id;
12223        let sketch = expect_sketch(sketch_object);
12224        let spline_id = sketch
12225            .segments
12226            .iter()
12227            .copied()
12228            .find(|seg_id| {
12229                matches!(
12230                    &frontend.scene_graph.objects[seg_id.0].kind,
12231                    ObjectKind::Segment {
12232                        segment: Segment::ControlPointSpline(_)
12233                    }
12234                )
12235            })
12236            .expect("Expected a control point spline segment in sketch");
12237        let edge_id = frontend
12238            .scene_graph
12239            .objects
12240            .iter()
12241            .find_map(|obj| match &obj.kind {
12242                ObjectKind::Segment {
12243                    segment: Segment::Line(line),
12244                } if line.owner == Some(spline_id) => Some(obj.id),
12245                _ => None,
12246            })
12247            .expect("Expected an owned control-polygon edge");
12248
12249        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12250        let (src_delta, _) = frontend
12251            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12252            .await
12253            .unwrap();
12254        assert!(
12255            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12256            "Expected horizontal constraint on spline edge, got: {}",
12257            src_delta.text
12258        );
12259
12260        ctx.close().await;
12261        mock_ctx.close().await;
12262    }
12263
12264    #[tokio::test(flavor = "multi_thread")]
12265    async fn test_control_point_spline_edge_angle() {
12266        let initial_source = "\
12267@settings(experimentalFeatures = allow)
12268splineSketch = sketch(on = XY) {
12269  controlPointSpline1 = controlPointSpline(points = [
12270    [var 0mm, var 0mm],
12271    [var 10mm, var 20mm],
12272    [var 20mm, var 0mm],
12273  ])
12274
12275  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12276}
12277";
12278
12279        let program = Program::parse(initial_source).unwrap().0.unwrap();
12280
12281        let mut frontend = FrontendState::new();
12282
12283        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12284        let mock_ctx = ExecutorContext::new_mock(None).await;
12285        let version = Version(0);
12286
12287        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12288        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12289        let sketch_id = sketch_object.id;
12290        let sketch = expect_sketch(sketch_object);
12291        let spline_id = sketch
12292            .segments
12293            .iter()
12294            .copied()
12295            .find(|seg_id| {
12296                matches!(
12297                    &frontend.scene_graph.objects[seg_id.0].kind,
12298                    ObjectKind::Segment {
12299                        segment: Segment::ControlPointSpline(_)
12300                    }
12301                )
12302            })
12303            .expect("Expected a control point spline segment in sketch");
12304        let edge_id = frontend
12305            .scene_graph
12306            .objects
12307            .iter()
12308            .find_map(|obj| match &obj.kind {
12309                ObjectKind::Segment {
12310                    segment: Segment::Line(line),
12311                } if line.owner == Some(spline_id) => Some(obj.id),
12312                _ => None,
12313            })
12314            .expect("Expected an owned control-polygon edge");
12315        let line1_id = frontend
12316            .scene_graph
12317            .objects
12318            .iter()
12319            .find_map(|obj| match &obj.kind {
12320                ObjectKind::Segment {
12321                    segment: Segment::Line(line),
12322                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12323                _ => None,
12324            })
12325            .or_else(|| {
12326                sketch.segments.iter().copied().find(|seg_id| {
12327                    matches!(
12328                        &frontend.scene_graph.objects[seg_id.0].kind,
12329                        ObjectKind::Segment {
12330                            segment: Segment::Line(line),
12331                        } if line.owner.is_none()
12332                    )
12333                })
12334            })
12335            .expect("Expected a standalone line segment in sketch");
12336
12337        let constraint = Constraint::Angle(Angle {
12338            lines: vec![line1_id, edge_id],
12339            angle: Number {
12340                value: 30.0,
12341                units: NumericSuffix::Deg,
12342            },
12343            source: Default::default(),
12344        });
12345        let (src_delta, _) = frontend
12346            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12347            .await
12348            .unwrap();
12349        assert!(
12350            src_delta
12351                .text
12352                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12353            "Expected angle constraint on spline edge, got: {}",
12354            src_delta.text
12355        );
12356
12357        ctx.close().await;
12358        mock_ctx.close().await;
12359    }
12360
12361    #[tokio::test(flavor = "multi_thread")]
12362    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12363        let initial_source = "\
12364@settings(experimentalFeatures = allow)
12365splineSketch = sketch(on = XY) {
12366  spline1 = controlPointSpline(points = [
12367    [var 0mm, var 0mm],
12368    [var 10mm, var 20mm],
12369    [var 20mm, var 0mm],
12370  ])
12371  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12372  coincident([spline1.controls[1], spline1.edges[0]])
12373  coincident([spline1.controls[0], line1])
12374}
12375";
12376
12377        let program = Program::parse(initial_source).unwrap().0.unwrap();
12378
12379        let mut frontend = FrontendState::new();
12380
12381        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12382        let mock_ctx = ExecutorContext::new_mock(None).await;
12383
12384        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12385
12386        let ui_scene_graph = frontend.scene_graph_for_ui();
12387        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12388        let sketch = expect_sketch(sketch_object);
12389
12390        assert_eq!(
12391            sketch.constraints.len(),
12392            1,
12393            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12394        );
12395
12396        let visible_constraints = ui_scene_graph
12397            .objects
12398            .iter()
12399            .filter_map(|object| match &object.kind {
12400                ObjectKind::Constraint {
12401                    constraint: Constraint::Coincident(coincident),
12402                } => Some(coincident.clone()),
12403                _ => None,
12404            })
12405            .collect::<Vec<_>>();
12406
12407        assert_eq!(
12408            visible_constraints.len(),
12409            1,
12410            "Expected only one coincident constraint object in the UI scene graph"
12411        );
12412        assert_eq!(
12413            visible_constraints[0].get_segments().len(),
12414            2,
12415            "Expected the remaining visible coincident constraint to reference two segments"
12416        );
12417
12418        ctx.close().await;
12419        mock_ctx.close().await;
12420    }
12421
12422    #[tokio::test(flavor = "multi_thread")]
12423    async fn test_edit_control_point_spline_can_append_control_point() {
12424        let initial_source = "\
12425@settings(experimentalFeatures = allow)
12426splineSketch = sketch(on = XY) {
12427  controlPointSpline(points = [
12428    [var 0mm, var 0mm],
12429    [var 10mm, var 20mm],
12430    [var 20mm, var 0mm],
12431  ])
12432}
12433";
12434
12435        let program = Program::parse(initial_source).unwrap().0.unwrap();
12436
12437        let mut frontend = FrontendState::new();
12438
12439        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12440        let mock_ctx = ExecutorContext::new_mock(None).await;
12441        let version = Version(0);
12442
12443        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12444        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12445        let sketch_id = sketch_object.id;
12446        let sketch = expect_sketch(sketch_object);
12447        let spline_id = sketch
12448            .segments
12449            .iter()
12450            .copied()
12451            .find(|seg_id| {
12452                matches!(
12453                    &frontend.scene_graph.objects[seg_id.0].kind,
12454                    ObjectKind::Segment {
12455                        segment: Segment::ControlPointSpline(_)
12456                    }
12457                )
12458            })
12459            .expect("Expected a control point spline segment in sketch");
12460
12461        let ctor = ControlPointSplineCtor {
12462            points: vec![
12463                Point2d {
12464                    x: Expr::Var(Number {
12465                        value: 0.0,
12466                        units: NumericSuffix::Mm,
12467                    }),
12468                    y: Expr::Var(Number {
12469                        value: 0.0,
12470                        units: NumericSuffix::Mm,
12471                    }),
12472                },
12473                Point2d {
12474                    x: Expr::Var(Number {
12475                        value: 10.0,
12476                        units: NumericSuffix::Mm,
12477                    }),
12478                    y: Expr::Var(Number {
12479                        value: 20.0,
12480                        units: NumericSuffix::Mm,
12481                    }),
12482                },
12483                Point2d {
12484                    x: Expr::Var(Number {
12485                        value: 20.0,
12486                        units: NumericSuffix::Mm,
12487                    }),
12488                    y: Expr::Var(Number {
12489                        value: 0.0,
12490                        units: NumericSuffix::Mm,
12491                    }),
12492                },
12493                Point2d {
12494                    x: Expr::Var(Number {
12495                        value: 30.0,
12496                        units: NumericSuffix::Mm,
12497                    }),
12498                    y: Expr::Var(Number {
12499                        value: 10.0,
12500                        units: NumericSuffix::Mm,
12501                    }),
12502                },
12503            ],
12504            construction: None,
12505        };
12506
12507        let segments = vec![ExistingSegmentCtor {
12508            id: spline_id,
12509            ctor: SegmentCtor::ControlPointSpline(ctor),
12510        }];
12511        let (src_delta, scene_delta) = frontend
12512            .edit_segments(&mock_ctx, version, sketch_id, segments)
12513            .await
12514            .unwrap();
12515
12516        assert!(
12517            src_delta.text.contains("[var 30mm, var 10mm]"),
12518            "Expected appended spline control point in source, got: {}",
12519            src_delta.text
12520        );
12521
12522        assert!(
12523            scene_delta.invalidates_ids,
12524            "Expected appending a spline control point to invalidate ids"
12525        );
12526        let updated_spline = scene_delta
12527            .new_graph
12528            .objects
12529            .iter()
12530            .find_map(|obj| match &obj.kind {
12531                ObjectKind::Segment {
12532                    segment: Segment::ControlPointSpline(updated_spline),
12533                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12534                _ => None,
12535            })
12536            .expect("Expected edited scene graph to contain a four-point control point spline");
12537        assert_eq!(
12538            updated_spline.controls.len(),
12539            4,
12540            "Expected edited spline to expose four control points"
12541        );
12542
12543        ctx.close().await;
12544        mock_ctx.close().await;
12545    }
12546
12547    #[tokio::test(flavor = "multi_thread")]
12548    async fn test_line_vertical() {
12549        let initial_source = "\
12550sketch(on = XY) {
12551  line(start = [var 1, var 2], end = [var 3, var 4])
12552}
12553";
12554
12555        let program = Program::parse(initial_source).unwrap().0.unwrap();
12556
12557        let mut frontend = FrontendState::new();
12558
12559        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12560        let mock_ctx = ExecutorContext::new_mock(None).await;
12561        let version = Version(0);
12562
12563        frontend.hack_set_program(&ctx, program).await.unwrap();
12564        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12565        let sketch_id = sketch_object.id;
12566        let sketch = expect_sketch(sketch_object);
12567        let line1_id = *sketch.segments.get(2).unwrap();
12568
12569        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12570        let (src_delta, scene_delta) = frontend
12571            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12572            .await
12573            .unwrap();
12574        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
12575        assert_eq!(
12576            scene_delta.new_graph.objects.len(),
12577            6,
12578            "{:#?}",
12579            scene_delta.new_graph.objects
12580        );
12581
12582        ctx.close().await;
12583        mock_ctx.close().await;
12584    }
12585
12586    #[tokio::test(flavor = "multi_thread")]
12587    async fn test_points_vertical() {
12588        let initial_source = "\
12589sketch001 = sketch(on = XY) {
12590  p0 = point(at = [var -2.23mm, var 3.1mm])
12591  pf = point(at = [4, 4])
12592}
12593";
12594
12595        let program = Program::parse(initial_source).unwrap().0.unwrap();
12596
12597        let mut frontend = FrontendState::new();
12598
12599        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12600        let mock_ctx = ExecutorContext::new_mock(None).await;
12601        let version = Version(0);
12602
12603        frontend.hack_set_program(&ctx, program).await.unwrap();
12604        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12605        let sketch_id = sketch_object.id;
12606        let sketch = expect_sketch(sketch_object);
12607        let point_ids = vec![
12608            sketch.segments.first().unwrap().to_owned(),
12609            sketch.segments.get(1).unwrap().to_owned(),
12610        ];
12611
12612        let constraint = Constraint::Vertical(Vertical::Points {
12613            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12614        });
12615        let (src_delta, scene_delta) = frontend
12616            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12617            .await
12618            .unwrap();
12619        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
12620        assert_eq!(
12621            scene_delta.new_graph.objects.len(),
12622            5,
12623            "{:#?}",
12624            scene_delta.new_graph.objects
12625        );
12626
12627        ctx.close().await;
12628        mock_ctx.close().await;
12629    }
12630
12631    #[tokio::test(flavor = "multi_thread")]
12632    async fn test_points_horizontal() {
12633        let initial_source = "\
12634sketch001 = sketch(on = XY) {
12635  p0 = point(at = [var -2.23mm, var 3.1mm])
12636  pf = point(at = [4, 4])
12637}
12638";
12639
12640        let program = Program::parse(initial_source).unwrap().0.unwrap();
12641
12642        let mut frontend = FrontendState::new();
12643
12644        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12645        let mock_ctx = ExecutorContext::new_mock(None).await;
12646        let version = Version(0);
12647
12648        frontend.hack_set_program(&ctx, program).await.unwrap();
12649        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12650        let sketch_id = sketch_object.id;
12651        let sketch = expect_sketch(sketch_object);
12652        let point_ids = vec![
12653            sketch.segments.first().unwrap().to_owned(),
12654            sketch.segments.get(1).unwrap().to_owned(),
12655        ];
12656
12657        let constraint = Constraint::Horizontal(Horizontal::Points {
12658            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12659        });
12660        let (src_delta, scene_delta) = frontend
12661            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12662            .await
12663            .unwrap();
12664        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
12665        assert_eq!(
12666            scene_delta.new_graph.objects.len(),
12667            5,
12668            "{:#?}",
12669            scene_delta.new_graph.objects
12670        );
12671
12672        ctx.close().await;
12673        mock_ctx.close().await;
12674    }
12675
12676    #[tokio::test(flavor = "multi_thread")]
12677    async fn test_point_horizontal_with_origin() {
12678        let initial_source = "\
12679sketch001 = sketch(on = XY) {
12680  p0 = point(at = [var -2.23mm, var 3.1mm])
12681}
12682";
12683
12684        let program = Program::parse(initial_source).unwrap().0.unwrap();
12685
12686        let mut frontend = FrontendState::new();
12687
12688        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12689        let mock_ctx = ExecutorContext::new_mock(None).await;
12690        let version = Version(0);
12691
12692        frontend.hack_set_program(&ctx, program).await.unwrap();
12693        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12694        let sketch_id = sketch_object.id;
12695        let sketch = expect_sketch(sketch_object);
12696        let point_id = *sketch.segments.first().unwrap();
12697
12698        let constraint = Constraint::Horizontal(Horizontal::Points {
12699            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12700        });
12701        let (src_delta, scene_delta) = frontend
12702            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12703            .await
12704            .unwrap();
12705        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
12706        assert_eq!(
12707            scene_delta.new_graph.objects.len(),
12708            4,
12709            "{:#?}",
12710            scene_delta.new_graph.objects
12711        );
12712
12713        ctx.close().await;
12714        mock_ctx.close().await;
12715    }
12716
12717    #[tokio::test(flavor = "multi_thread")]
12718    async fn test_lines_equal_length() {
12719        let initial_source = "\
12720sketch(on = XY) {
12721  line(start = [var 1, var 2], end = [var 3, var 4])
12722  line(start = [var 5, var 6], end = [var 7, var 8])
12723}
12724";
12725
12726        let program = Program::parse(initial_source).unwrap().0.unwrap();
12727
12728        let mut frontend = FrontendState::new();
12729
12730        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12731        let mock_ctx = ExecutorContext::new_mock(None).await;
12732        let version = Version(0);
12733
12734        frontend.hack_set_program(&ctx, program).await.unwrap();
12735        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12736        let sketch_id = sketch_object.id;
12737        let sketch = expect_sketch(sketch_object);
12738        let line1_id = *sketch.segments.get(2).unwrap();
12739        let line2_id = *sketch.segments.get(5).unwrap();
12740
12741        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12742            lines: vec![line1_id, line2_id],
12743        });
12744        let (src_delta, scene_delta) = frontend
12745            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12746            .await
12747            .unwrap();
12748        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
12749        assert_eq!(
12750            scene_delta.new_graph.objects.len(),
12751            9,
12752            "{:#?}",
12753            scene_delta.new_graph.objects
12754        );
12755
12756        ctx.close().await;
12757        mock_ctx.close().await;
12758    }
12759
12760    #[tokio::test(flavor = "multi_thread")]
12761    async fn test_add_constraint_multi_line_equal_length() {
12762        let initial_source = "\
12763sketch(on = XY) {
12764  line(start = [var 1, var 2], end = [var 3, var 4])
12765  line(start = [var 5, var 6], end = [var 7, var 8])
12766  line(start = [var 9, var 10], end = [var 11, var 12])
12767}
12768";
12769
12770        let program = Program::parse(initial_source).unwrap().0.unwrap();
12771
12772        let mut frontend = FrontendState::new();
12773        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12774        let mock_ctx = ExecutorContext::new_mock(None).await;
12775        let version = Version(0);
12776
12777        frontend.hack_set_program(&ctx, program).await.unwrap();
12778        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12779        let sketch_id = sketch_object.id;
12780        let sketch = expect_sketch(sketch_object);
12781        let line1_id = *sketch.segments.get(2).unwrap();
12782        let line2_id = *sketch.segments.get(5).unwrap();
12783        let line3_id = *sketch.segments.get(8).unwrap();
12784
12785        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12786            lines: vec![line1_id, line2_id, line3_id],
12787        });
12788        let (src_delta, scene_delta) = frontend
12789            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12790            .await
12791            .unwrap();
12792        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
12793        let constraints = scene_delta
12794            .new_graph
12795            .objects
12796            .iter()
12797            .filter_map(|obj| {
12798                let ObjectKind::Constraint { constraint } = &obj.kind else {
12799                    return None;
12800                };
12801                Some(constraint)
12802            })
12803            .collect::<Vec<_>>();
12804
12805        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12806        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12807            panic!("expected equal length constraint, got {:?}", constraints[0]);
12808        };
12809        assert_eq!(lines_equal_length.lines.len(), 3);
12810
12811        ctx.close().await;
12812        mock_ctx.close().await;
12813    }
12814
12815    #[tokio::test(flavor = "multi_thread")]
12816    async fn test_lines_parallel() {
12817        let initial_source = "\
12818sketch(on = XY) {
12819  line(start = [var 1, var 2], end = [var 3, var 4])
12820  line(start = [var 5, var 6], end = [var 7, var 8])
12821}
12822";
12823
12824        let program = Program::parse(initial_source).unwrap().0.unwrap();
12825
12826        let mut frontend = FrontendState::new();
12827
12828        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12829        let mock_ctx = ExecutorContext::new_mock(None).await;
12830        let version = Version(0);
12831
12832        frontend.hack_set_program(&ctx, program).await.unwrap();
12833        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12834        let sketch_id = sketch_object.id;
12835        let sketch = expect_sketch(sketch_object);
12836        let line1_id = *sketch.segments.get(2).unwrap();
12837        let line2_id = *sketch.segments.get(5).unwrap();
12838
12839        let constraint = Constraint::Parallel(Parallel {
12840            lines: vec![line1_id, line2_id],
12841        });
12842        let (src_delta, scene_delta) = frontend
12843            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12844            .await
12845            .unwrap();
12846        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
12847        assert_eq!(
12848            scene_delta.new_graph.objects.len(),
12849            9,
12850            "{:#?}",
12851            scene_delta.new_graph.objects
12852        );
12853
12854        ctx.close().await;
12855        mock_ctx.close().await;
12856    }
12857
12858    #[tokio::test(flavor = "multi_thread")]
12859    async fn test_lines_parallel_multiline() {
12860        let initial_source = "\
12861sketch(on = XY) {
12862  line(start = [var 1, var 2], end = [var 3, var 4])
12863  line(start = [var 5, var 6], end = [var 7, var 8])
12864  line(start = [var 9, var 10], end = [var 11, var 12])
12865}
12866";
12867
12868        let program = Program::parse(initial_source).unwrap().0.unwrap();
12869
12870        let mut frontend = FrontendState::new();
12871
12872        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12873        let mock_ctx = ExecutorContext::new_mock(None).await;
12874        let version = Version(0);
12875
12876        frontend.hack_set_program(&ctx, program).await.unwrap();
12877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878        let sketch_id = sketch_object.id;
12879        let sketch = expect_sketch(sketch_object);
12880        let line1_id = *sketch.segments.get(2).unwrap();
12881        let line2_id = *sketch.segments.get(5).unwrap();
12882        let line3_id = *sketch.segments.get(8).unwrap();
12883
12884        let constraint = Constraint::Parallel(Parallel {
12885            lines: vec![line1_id, line2_id, line3_id],
12886        });
12887        let (src_delta, scene_delta) = frontend
12888            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12889            .await
12890            .unwrap();
12891        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
12892
12893        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12894        let sketch = expect_sketch(sketch_object);
12895        assert_eq!(sketch.constraints.len(), 1);
12896
12897        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12898        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12899            panic!("Expected constraint object");
12900        };
12901        let Constraint::Parallel(parallel) = constraint else {
12902            panic!("Expected parallel constraint");
12903        };
12904        assert_eq!(parallel.lines.len(), 3);
12905
12906        ctx.close().await;
12907        mock_ctx.close().await;
12908    }
12909
12910    #[tokio::test(flavor = "multi_thread")]
12911    async fn test_lines_perpendicular() {
12912        let initial_source = "\
12913sketch(on = XY) {
12914  line(start = [var 1, var 2], end = [var 3, var 4])
12915  line(start = [var 5, var 6], end = [var 7, var 8])
12916}
12917";
12918
12919        let program = Program::parse(initial_source).unwrap().0.unwrap();
12920
12921        let mut frontend = FrontendState::new();
12922
12923        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12924        let mock_ctx = ExecutorContext::new_mock(None).await;
12925        let version = Version(0);
12926
12927        frontend.hack_set_program(&ctx, program).await.unwrap();
12928        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12929        let sketch_id = sketch_object.id;
12930        let sketch = expect_sketch(sketch_object);
12931        let line1_id = *sketch.segments.get(2).unwrap();
12932        let line2_id = *sketch.segments.get(5).unwrap();
12933
12934        let constraint = Constraint::Perpendicular(Perpendicular {
12935            lines: vec![line1_id, line2_id],
12936        });
12937        let (src_delta, scene_delta) = frontend
12938            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12939            .await
12940            .unwrap();
12941        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
12942        assert_eq!(
12943            scene_delta.new_graph.objects.len(),
12944            9,
12945            "{:#?}",
12946            scene_delta.new_graph.objects
12947        );
12948
12949        ctx.close().await;
12950        mock_ctx.close().await;
12951    }
12952
12953    #[tokio::test(flavor = "multi_thread")]
12954    async fn test_lines_angle() {
12955        let initial_source = "\
12956sketch(on = XY) {
12957  line(start = [var 1, var 2], end = [var 3, var 4])
12958  line(start = [var 5, var 6], end = [var 7, var 8])
12959}
12960";
12961
12962        let program = Program::parse(initial_source).unwrap().0.unwrap();
12963
12964        let mut frontend = FrontendState::new();
12965
12966        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12967        let mock_ctx = ExecutorContext::new_mock(None).await;
12968        let version = Version(0);
12969
12970        frontend.hack_set_program(&ctx, program).await.unwrap();
12971        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12972        let sketch_id = sketch_object.id;
12973        let sketch = expect_sketch(sketch_object);
12974        let line1_id = *sketch.segments.get(2).unwrap();
12975        let line2_id = *sketch.segments.get(5).unwrap();
12976
12977        let constraint = Constraint::Angle(Angle {
12978            lines: vec![line1_id, line2_id],
12979            angle: Number {
12980                value: 30.0,
12981                units: NumericSuffix::Deg,
12982            },
12983            source: Default::default(),
12984        });
12985        let (src_delta, scene_delta) = frontend
12986            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12987            .await
12988            .unwrap();
12989        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
12990        assert_eq!(
12991            scene_delta.new_graph.objects.len(),
12992            9,
12993            "{:#?}",
12994            scene_delta.new_graph.objects
12995        );
12996
12997        ctx.close().await;
12998        mock_ctx.close().await;
12999    }
13000
13001    #[tokio::test(flavor = "multi_thread")]
13002    async fn test_segments_tangent() {
13003        let initial_source = "\
13004sketch(on = XY) {
13005  line(start = [var 1, var 2], end = [var 3, var 4])
13006  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13007}
13008";
13009
13010        let program = Program::parse(initial_source).unwrap().0.unwrap();
13011
13012        let mut frontend = FrontendState::new();
13013
13014        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13015        let mock_ctx = ExecutorContext::new_mock(None).await;
13016        let version = Version(0);
13017
13018        frontend.hack_set_program(&ctx, program).await.unwrap();
13019        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13020        let sketch_id = sketch_object.id;
13021        let sketch = expect_sketch(sketch_object);
13022        let line1_id = *sketch.segments.get(2).unwrap();
13023        let arc1_id = *sketch.segments.get(6).unwrap();
13024
13025        let constraint = Constraint::Tangent(Tangent {
13026            input: vec![line1_id, arc1_id],
13027        });
13028        let (src_delta, scene_delta) = frontend
13029            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13030            .await
13031            .unwrap();
13032        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13033        assert_eq!(
13034            scene_delta.new_graph.objects.len(),
13035            10,
13036            "{:#?}",
13037            scene_delta.new_graph.objects
13038        );
13039
13040        ctx.close().await;
13041        mock_ctx.close().await;
13042    }
13043
13044    #[tokio::test(flavor = "multi_thread")]
13045    async fn test_point_midpoint() {
13046        let initial_source = "\
13047sketch(on = XY) {
13048  point(at = [var 1, var 1])
13049  line(start = [var 0, var 0], end = [var 6, var 4])
13050}
13051";
13052
13053        let program = Program::parse(initial_source).unwrap().0.unwrap();
13054
13055        let mut frontend = FrontendState::new();
13056
13057        let ctx = ExecutorContext::new_mock(None).await;
13058        let version = Version(0);
13059
13060        frontend.program = program.clone();
13061        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13062        frontend.update_state_after_exec(outcome, true);
13063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13064        let sketch_id = sketch_object.id;
13065        let sketch = expect_sketch(sketch_object);
13066        let point_id = *sketch.segments.first().unwrap();
13067        let line_id = *sketch.segments.get(3).unwrap();
13068
13069        let constraint = Constraint::Midpoint(Midpoint {
13070            point: ConstraintSegment::from(point_id),
13071            segment: line_id,
13072        });
13073        let (src_delta, scene_delta) = frontend
13074            .add_constraint(&ctx, version, sketch_id, constraint)
13075            .await
13076            .unwrap();
13077        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13078        assert_eq!(
13079            scene_delta.new_graph.objects.len(),
13080            7,
13081            "{:#?}",
13082            scene_delta.new_graph.objects
13083        );
13084
13085        ctx.close().await;
13086    }
13087
13088    #[tokio::test(flavor = "multi_thread")]
13089    async fn test_segments_symmetric() {
13090        let initial_source = "\
13091sketch(on = XY) {
13092  line(start = [var 0, var 0], end = [var 0, var 4])
13093  line(start = [var 4, var 0], end = [var 4, var 4])
13094  line(start = [var 2, var -1], end = [var 2, var 5])
13095}
13096";
13097
13098        let program = Program::parse(initial_source).unwrap().0.unwrap();
13099
13100        let mut frontend = FrontendState::new();
13101
13102        let ctx = ExecutorContext::new_mock(None).await;
13103        let version = Version(0);
13104
13105        frontend.program = program.clone();
13106        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13107        frontend.update_state_after_exec(outcome, true);
13108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13109        let sketch_id = sketch_object.id;
13110        let sketch = expect_sketch(sketch_object);
13111        let line1_id = *sketch.segments.get(2).unwrap();
13112        let line2_id = *sketch.segments.get(5).unwrap();
13113        let axis_id = *sketch.segments.get(8).unwrap();
13114
13115        let constraint = Constraint::Symmetric(Symmetric {
13116            input: vec![line1_id, line2_id],
13117            axis: axis_id,
13118        });
13119        let (src_delta, scene_delta) = frontend
13120            .add_constraint(&ctx, version, sketch_id, constraint)
13121            .await
13122            .unwrap();
13123        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
13124        assert_eq!(
13125            scene_delta.new_graph.objects.len(),
13126            12,
13127            "{:#?}",
13128            scene_delta.new_graph.objects
13129        );
13130
13131        ctx.close().await;
13132    }
13133
13134    #[tokio::test(flavor = "multi_thread")]
13135    async fn test_point_arc_midpoint() {
13136        let initial_source = "\
13137sketch(on = XY) {
13138  point(at = [var 6, var 3])
13139  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13140}
13141";
13142
13143        let program = Program::parse(initial_source).unwrap().0.unwrap();
13144
13145        let mut frontend = FrontendState::new();
13146
13147        let ctx = ExecutorContext::new_mock(None).await;
13148        let version = Version(0);
13149
13150        frontend.program = program.clone();
13151        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13152        frontend.update_state_after_exec(outcome, true);
13153        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13154        let sketch_id = sketch_object.id;
13155        let sketch = expect_sketch(sketch_object);
13156        let point_id = *sketch.segments.first().unwrap();
13157        let arc_id = *sketch.segments.get(4).unwrap();
13158
13159        let constraint = Constraint::Midpoint(Midpoint {
13160            point: ConstraintSegment::from(point_id),
13161            segment: arc_id,
13162        });
13163        let (src_delta, scene_delta) = frontend
13164            .add_constraint(&ctx, version, sketch_id, constraint)
13165            .await
13166            .unwrap();
13167        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
13168        assert_eq!(
13169            scene_delta.new_graph.objects.len(),
13170            8,
13171            "{:#?}",
13172            scene_delta.new_graph.objects
13173        );
13174
13175        ctx.close().await;
13176    }
13177
13178    #[tokio::test(flavor = "multi_thread")]
13179    async fn test_origin_line_midpoint() {
13180        let initial_source = "\
13181sketch(on = XY) {
13182  line(start = [var 0, var 0], end = [var 6, var 4])
13183}
13184";
13185
13186        let program = Program::parse(initial_source).unwrap().0.unwrap();
13187
13188        let mut frontend = FrontendState::new();
13189
13190        let ctx = ExecutorContext::new_mock(None).await;
13191        let version = Version(0);
13192
13193        frontend.program = program.clone();
13194        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13195        frontend.update_state_after_exec(outcome, true);
13196        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13197        let sketch_id = sketch_object.id;
13198        let sketch = expect_sketch(sketch_object);
13199        let line_id = *sketch.segments.get(2).unwrap();
13200
13201        let constraint = Constraint::Midpoint(Midpoint {
13202            point: ConstraintSegment::ORIGIN,
13203            segment: line_id,
13204        });
13205        let (src_delta, scene_delta) = frontend
13206            .add_constraint(&ctx, version, sketch_id, constraint)
13207            .await
13208            .unwrap();
13209        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
13210        assert_eq!(
13211            scene_delta.new_graph.objects.len(),
13212            6,
13213            "{:#?}",
13214            scene_delta.new_graph.objects
13215        );
13216
13217        ctx.close().await;
13218    }
13219
13220    #[tokio::test(flavor = "multi_thread")]
13221    async fn test_origin_arc_midpoint() {
13222        let initial_source = "\
13223sketch(on = XY) {
13224  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13225}
13226";
13227
13228        let program = Program::parse(initial_source).unwrap().0.unwrap();
13229
13230        let mut frontend = FrontendState::new();
13231
13232        let ctx = ExecutorContext::new_mock(None).await;
13233        let version = Version(0);
13234
13235        frontend.program = program.clone();
13236        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13237        frontend.update_state_after_exec(outcome, true);
13238        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13239        let sketch_id = sketch_object.id;
13240        let sketch = expect_sketch(sketch_object);
13241        let arc_id = *sketch.segments.get(3).unwrap();
13242
13243        let constraint = Constraint::Midpoint(Midpoint {
13244            point: ConstraintSegment::ORIGIN,
13245            segment: arc_id,
13246        });
13247        let (src_delta, scene_delta) = frontend
13248            .add_constraint(&ctx, version, sketch_id, constraint)
13249            .await
13250            .unwrap();
13251        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
13252        assert_eq!(
13253            scene_delta.new_graph.objects.len(),
13254            7,
13255            "{:#?}",
13256            scene_delta.new_graph.objects
13257        );
13258
13259        ctx.close().await;
13260    }
13261
13262    #[tokio::test(flavor = "multi_thread")]
13263    async fn test_segments_symmetric_arcs() {
13264        let initial_source = "\
13265sketch(on = XY) {
13266  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13267  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13268  line(start = [var 0, var -10], end = [var 0, var 10])
13269}
13270";
13271
13272        let program = Program::parse(initial_source).unwrap().0.unwrap();
13273
13274        let mut frontend = FrontendState::new();
13275
13276        let ctx = ExecutorContext::new_mock(None).await;
13277        let version = Version(0);
13278
13279        frontend.program = program.clone();
13280        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13281        frontend.update_state_after_exec(outcome, true);
13282        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13283        let sketch_id = sketch_object.id;
13284        let sketch = expect_sketch(sketch_object);
13285        let arc1_id = *sketch.segments.get(3).unwrap();
13286        let arc2_id = *sketch.segments.get(7).unwrap();
13287        let axis_id = *sketch.segments.get(10).unwrap();
13288
13289        let constraint = Constraint::Symmetric(Symmetric {
13290            input: vec![arc1_id, arc2_id],
13291            axis: axis_id,
13292        });
13293        let (src_delta, scene_delta) = frontend
13294            .add_constraint(&ctx, version, sketch_id, constraint)
13295            .await
13296            .unwrap();
13297        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
13298        assert_eq!(
13299            scene_delta.new_graph.objects.len(),
13300            14,
13301            "{:#?}",
13302            scene_delta.new_graph.objects
13303        );
13304
13305        ctx.close().await;
13306    }
13307
13308    #[tokio::test(flavor = "multi_thread")]
13309    async fn test_sketch_on_face_simple() {
13310        let initial_source = "\
13311len = 2mm
13312cube = startSketchOn(XY)
13313  |> startProfile(at = [0, 0])
13314  |> line(end = [len, 0], tag = $side)
13315  |> line(end = [0, len])
13316  |> line(end = [-len, 0])
13317  |> line(end = [0, -len])
13318  |> close()
13319  |> extrude(length = len)
13320
13321face = faceOf(cube, face = side)
13322";
13323
13324        let program = Program::parse(initial_source).unwrap().0.unwrap();
13325
13326        let mut frontend = FrontendState::new();
13327
13328        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13329        let mock_ctx = ExecutorContext::new_mock(None).await;
13330        let version = Version(0);
13331
13332        frontend.hack_set_program(&ctx, program).await.unwrap();
13333        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13334        let face_id = face_object.id;
13335
13336        let sketch_args = SketchCtor {
13337            on: Plane::Object(face_id),
13338        };
13339        let (_src_delta, scene_delta, sketch_id) = frontend
13340            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13341            .await
13342            .unwrap();
13343        assert_eq!(sketch_id, ObjectId(2));
13344        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13345        let sketch_object = &scene_delta.new_graph.objects[2];
13346        assert_eq!(sketch_object.id, ObjectId(2));
13347        assert_eq!(
13348            sketch_object.kind,
13349            ObjectKind::Sketch(Sketch {
13350                args: SketchCtor {
13351                    on: Plane::Object(face_id),
13352                },
13353                plane: face_id,
13354                segments: vec![],
13355                constraints: vec![],
13356            })
13357        );
13358        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13359
13360        ctx.close().await;
13361        mock_ctx.close().await;
13362    }
13363
13364    #[tokio::test(flavor = "multi_thread")]
13365    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13366        let initial_source = "\
13367s = sketch(on = YZ) {
13368  line1 = line(start = [0, 0], end = [0, 1])
13369  line2 = line(start = [0, 1], end = [1, 1])
13370  line3 = line(start = [1, 1], end = [0, 0])
13371}
13372region001 = region(point = [0.1, 0.1], sketch = s)
13373extrude001 = extrude(region001, length = 5)
13374";
13375
13376        let program = Program::parse(initial_source).unwrap().0.unwrap();
13377
13378        let mut frontend = FrontendState::new();
13379        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13380        let version = Version(0);
13381
13382        frontend.hack_set_program(&ctx, program).await.unwrap();
13383        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13384
13385        let sketch_args = SketchCtor {
13386            on: Plane::Object(wall_object_id),
13387        };
13388        let (src_delta, _scene_delta, _sketch_id) = frontend
13389            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13390            .await
13391            .unwrap();
13392        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13393
13394        ctx.close().await;
13395    }
13396
13397    #[tokio::test(flavor = "multi_thread")]
13398    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13399        let initial_source = "\
13400sketch001 = sketch(on = YZ) {
13401  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13402  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13403  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13404  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13405  coincident([line1.end, line2.start])
13406  coincident([line2.end, line3.start])
13407  coincident([line3.end, line4.start])
13408  coincident([line4.end, line1.start])
13409  parallel([line2, line4])
13410  parallel([line3, line1])
13411  perpendicular([line1, line2])
13412  horizontal(line3)
13413  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13414}
13415region001 = region(point = [3.1, 3.74], sketch = sketch001)
13416extrude001 = extrude(region001, length = 5)
13417";
13418
13419        let program = Program::parse(initial_source).unwrap().0.unwrap();
13420
13421        let mut frontend = FrontendState::new();
13422        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13423        let version = Version(0);
13424
13425        frontend.hack_set_program(&ctx, program).await.unwrap();
13426        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13427
13428        let sketch_args = SketchCtor {
13429            on: Plane::Object(wall_object_id),
13430        };
13431        let (src_delta, _scene_delta, _sketch_id) = frontend
13432            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13433            .await
13434            .unwrap();
13435        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13436
13437        ctx.close().await;
13438    }
13439
13440    #[tokio::test(flavor = "multi_thread")]
13441    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
13442        let initial_source = "\
13443@settings(kclVersion = 2.0)
13444
13445sketch001 = sketch(on = XY) {
13446  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
13447  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
13448}
13449hidden001 = hide(sketch001)
13450region001 = region(segments = [sketch001.circle2])
13451region002 = region(segments = [sketch001.circle1])
13452extrude001 = extrude([region001, region002], length = 5)
13453";
13454
13455        let program = Program::parse(initial_source).unwrap().0.unwrap();
13456        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13457        let version = Version(0);
13458
13459        for (solid_output_index, expected_face) in [
13460            (0, "faceOf(extrude001[0], face = END)"),
13461            (1, "faceOf(extrude001[1], face = END)"),
13462        ] {
13463            let mut frontend = FrontendState::new();
13464            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
13465            let cap_object_id = find_cap_object_id_with_solid_output_index(
13466                &frontend.scene_graph,
13467                crate::frontend::api::CapKind::End,
13468                solid_output_index,
13469            )
13470            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
13471
13472            let sketch_args = SketchCtor {
13473                on: Plane::Object(cap_object_id),
13474            };
13475            let (src_delta, _scene_delta, _sketch_id) = frontend
13476                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13477                .await
13478                .unwrap();
13479
13480            assert!(
13481                src_delta.text.contains(expected_face),
13482                "expected `{expected_face}` in:\n{}",
13483                src_delta.text
13484            );
13485            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
13486        }
13487
13488        ctx.close().await;
13489    }
13490
13491    #[tokio::test(flavor = "multi_thread")]
13492    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
13493        let initial_source = "\
13494@settings(kclVersion = 2.0)
13495
13496sketch001 = sketch(on = XY) {
13497  rect1Line1 = line(start = [0, 0], end = [1, 0])
13498  rect1Line2 = line(start = [1, 0], end = [1, 1])
13499  rect1Line3 = line(start = [1, 1], end = [0, 1])
13500  rect1Line4 = line(start = [0, 1], end = [0, 0])
13501  rect2Line1 = line(start = [3, 0], end = [4, 0])
13502  rect2Line2 = line(start = [4, 0], end = [4, 1])
13503  rect2Line3 = line(start = [4, 1], end = [3, 1])
13504  rect2Line4 = line(start = [3, 1], end = [3, 0])
13505}
13506hidden001 = hide(sketch001)
13507region001 = region(segments = [
13508  sketch001.rect1Line4,
13509  sketch001.rect1Line1
13510])
13511region002 = region(segments = [
13512  sketch001.rect2Line4,
13513  sketch001.rect2Line1
13514])
13515extrude001 = extrude([region001, region002], length = 5)
13516";
13517
13518        let program = Program::parse(initial_source).unwrap().0.unwrap();
13519        let mut frontend = FrontendState::new();
13520        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13521        let version = Version(0);
13522
13523        frontend.hack_set_program(&ctx, program).await.unwrap();
13524        let region_call = "\
13525region(segments = [
13526  sketch001.rect1Line4,
13527  sketch001.rect1Line1
13528])";
13529        let region_call_start = initial_source.find(region_call).unwrap();
13530        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
13531        let segment_call = "line(start = [0, 0], end = [1, 0])";
13532        let segment_call_start = initial_source.find(segment_call).unwrap();
13533        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
13534        let wall_object_id = frontend
13535            .scene_graph
13536            .objects
13537            .iter()
13538            .find_map(|object| match &object.kind {
13539                ObjectKind::Wall(wall)
13540                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
13541                        && wall.source.segment.range == segment_range =>
13542                {
13543                    Some(object.id)
13544                }
13545                _ => None,
13546            })
13547            .expect("expected a wall object for region001.tags.rect1Line1");
13548
13549        let sketch_args = SketchCtor {
13550            on: Plane::Object(wall_object_id),
13551        };
13552        let (src_delta, _scene_delta, _sketch_id) = frontend
13553            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13554            .await
13555            .unwrap();
13556
13557        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
13558        assert!(
13559            src_delta.text.contains(expected_face),
13560            "expected `{expected_face}` in:\n{}",
13561            src_delta.text
13562        );
13563        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
13564
13565        ctx.close().await;
13566    }
13567
13568    #[test]
13569    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13570        let source = "\
13571sketch001 = sketch(on = XY) {
13572  line(start = [0, 0], end = [1, 0])
13573}
13574part = subtract(boxSolid, tools = [cutSolid])
13575  |> appearance(color = \"#8f96a3\")
13576";
13577        let ast = Program::parse(source).unwrap().0.unwrap().ast;
13578        let line_start = source.find("line").unwrap();
13579        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13580        let line_ref = SourceRef::Simple {
13581            range: [line_start, line_end, 0].into(),
13582            node_path: None,
13583        };
13584        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13585
13586        let subtract_start = source.find("subtract").unwrap();
13587        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13588        let subtract_ref = SourceRef::Simple {
13589            range: [subtract_start, subtract_end, 0].into(),
13590            node_path: None,
13591        };
13592        assert_eq!(
13593            variable_name_containing_source_ref(&ast, &subtract_ref),
13594            Some("part".to_owned())
13595        );
13596    }
13597
13598    #[tokio::test(flavor = "multi_thread")]
13599    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13600        clear_mem_cache().await;
13601        let source = "\
13602boxSolid = startSketchOn(XY)
13603  |> startProfile(at = [0, 0])
13604  |> line(end = [4, 0], tag = $bottomEdge)
13605  |> line(end = [0, 4])
13606  |> line(end = [-4, 0])
13607  |> close()
13608  |> extrude(length = 10)
13609cutSolid = startSketchOn(XY)
13610  |> startProfile(at = [1, 1])
13611  |> line(end = [1, 0])
13612  |> line(end = [0, 1])
13613  |> line(end = [-1, 0])
13614  |> close()
13615  |> extrude(length = 10)
13616part = subtract(boxSolid, tools = [cutSolid])
13617  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13618";
13619        let program = Program::parse(source).unwrap().0.unwrap();
13620        let mut frontend = FrontendState::new();
13621        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13622        match frontend.hack_set_program(&ctx, program).await.unwrap() {
13623            SetProgramOutcome::Success { .. } => {}
13624            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13625        }
13626
13627        let sweep_call_start = source.find("extrude").unwrap();
13628        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13629        let part_call_start = source.find("subtract").unwrap();
13630        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13631        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13632        let composite_range = [part_call_start, part_call_end, 0].into();
13633
13634        let cap_object = frontend
13635            .scene_graph
13636            .objects
13637            .iter()
13638            .find(|object| {
13639                matches!(
13640                    &object.kind,
13641                    ObjectKind::Cap(crate::frontend::api::Cap {
13642                        kind: crate::frontend::api::CapKind::End,
13643                        source,
13644                        ..
13645                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
13646                )
13647            })
13648            .expect("expected end cap object to trace through subtract and original extrude");
13649
13650        let mut ast = frontend.program.ast.clone();
13651        let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13652        let cap_face_decl = ast::VariableDeclaration::new(
13653            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13654            ast::ItemVisibility::Default,
13655            ast::VariableKind::Const,
13656        );
13657        ast.body
13658            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13659                cap_face_decl,
13660            ))));
13661        let generated_source = source_from_ast(&ast);
13662
13663        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13664        assert!(!generated_source.contains("faceOf(boxSolid"));
13665        let ast::Expr::CallExpressionKw(call) = cap_expr else {
13666            panic!("expected faceOf call");
13667        };
13668        assert_eq!(call.callee.name.name, "faceOf");
13669        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13670            panic!("expected solid name");
13671        };
13672        assert_eq!(solid_name.name.name, "part");
13673        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13674            panic!("expected face name");
13675        };
13676        assert_eq!(face_name.name.name, "END");
13677
13678        ctx.close().await;
13679    }
13680
13681    #[tokio::test(flavor = "multi_thread")]
13682    async fn test_sketch_on_plane_incremental() {
13683        let initial_source = "\
13684len = 2mm
13685cube = startSketchOn(XY)
13686  |> startProfile(at = [0, 0])
13687  |> line(end = [len, 0], tag = $side)
13688  |> line(end = [0, len])
13689  |> line(end = [-len, 0])
13690  |> line(end = [0, -len])
13691  |> close()
13692  |> extrude(length = len)
13693
13694plane = planeOf(cube, face = side)
13695";
13696
13697        let program = Program::parse(initial_source).unwrap().0.unwrap();
13698
13699        let mut frontend = FrontendState::new();
13700
13701        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13702        let mock_ctx = ExecutorContext::new_mock(None).await;
13703        let version = Version(0);
13704
13705        frontend.hack_set_program(&ctx, program).await.unwrap();
13706        // Find the last plane since the first plane is the XY plane.
13707        let plane_object = frontend
13708            .scene_graph
13709            .objects
13710            .iter()
13711            .rev()
13712            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13713            .unwrap();
13714        let plane_id = plane_object.id;
13715
13716        let sketch_args = SketchCtor {
13717            on: Plane::Object(plane_id),
13718        };
13719        let (src_delta, scene_delta, sketch_id) = frontend
13720            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13721            .await
13722            .unwrap();
13723        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
13724        assert_eq!(sketch_id, ObjectId(2));
13725        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13726        let sketch_object = &scene_delta.new_graph.objects[2];
13727        assert_eq!(sketch_object.id, ObjectId(2));
13728        assert_eq!(
13729            sketch_object.kind,
13730            ObjectKind::Sketch(Sketch {
13731                args: SketchCtor {
13732                    on: Plane::Object(plane_id),
13733                },
13734                plane: plane_id,
13735                segments: vec![],
13736                constraints: vec![],
13737            })
13738        );
13739        assert_eq!(scene_delta.new_graph.objects.len(), 9);
13740
13741        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13742        assert_eq!(plane_object.id, plane_id);
13743        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13744
13745        ctx.close().await;
13746        mock_ctx.close().await;
13747    }
13748
13749    #[tokio::test(flavor = "multi_thread")]
13750    async fn test_new_sketch_uses_unique_variable_name() {
13751        let initial_source = "\
13752sketch1 = sketch(on = XY) {
13753}
13754";
13755
13756        let program = Program::parse(initial_source).unwrap().0.unwrap();
13757
13758        let mut frontend = FrontendState::new();
13759        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13760        let version = Version(0);
13761
13762        frontend.hack_set_program(&ctx, program).await.unwrap();
13763
13764        let sketch_args = SketchCtor {
13765            on: Plane::Default(PlaneName::Yz),
13766        };
13767        let (src_delta, _, _) = frontend
13768            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13769            .await
13770            .unwrap();
13771
13772        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
13773
13774        ctx.close().await;
13775    }
13776
13777    #[tokio::test(flavor = "multi_thread")]
13778    async fn test_new_sketch_twice_using_same_plane() {
13779        let initial_source = "\
13780sketch1 = sketch(on = XY) {
13781}
13782";
13783
13784        let program = Program::parse(initial_source).unwrap().0.unwrap();
13785
13786        let mut frontend = FrontendState::new();
13787        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13788        let version = Version(0);
13789
13790        frontend.hack_set_program(&ctx, program).await.unwrap();
13791
13792        let sketch_args = SketchCtor {
13793            on: Plane::Default(PlaneName::Xy),
13794        };
13795        let (src_delta, _, _) = frontend
13796            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13797            .await
13798            .unwrap();
13799
13800        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
13801
13802        ctx.close().await;
13803    }
13804
13805    #[tokio::test(flavor = "multi_thread")]
13806    async fn test_sketch_mode_reuses_cached_on_expression() {
13807        let initial_source = "\
13808width = 2mm
13809sketch(on = offsetPlane(XY, offset = width)) {
13810  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13811  distance([line1.start, line1.end]) == width
13812}
13813";
13814        let program = Program::parse(initial_source).unwrap().0.unwrap();
13815
13816        let mut frontend = FrontendState::new();
13817        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13818        let mock_ctx = ExecutorContext::new_mock(None).await;
13819        let version = Version(0);
13820        let project_id = ProjectId(0);
13821        let file_id = FileId(0);
13822
13823        frontend.hack_set_program(&ctx, program).await.unwrap();
13824        let initial_object_count = frontend.scene_graph.objects.len();
13825        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13826            .expect("Expected sketch object to exist")
13827            .id;
13828
13829        // Entering sketch mode should reuse cached `on` expression state
13830        // (offsetPlane result), not fail or create extra on-surface objects.
13831        let scene_delta = frontend
13832            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13833            .await
13834            .unwrap();
13835        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13836
13837        // A follow-up sketch-mode execution should keep the same stable object
13838        // graph shape as well.
13839        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13840        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13841
13842        ctx.close().await;
13843        mock_ctx.close().await;
13844    }
13845
13846    #[tokio::test(flavor = "multi_thread")]
13847    async fn test_multiple_sketch_blocks() {
13848        let initial_source = "\
13849// Cube that requires the engine.
13850width = 2
13851sketch001 = startSketchOn(XY)
13852profile001 = startProfile(sketch001, at = [0, 0])
13853  |> yLine(length = width, tag = $seg1)
13854  |> xLine(length = width)
13855  |> yLine(length = -width)
13856  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13857  |> close()
13858extrude001 = extrude(profile001, length = width)
13859
13860// Get a value that requires the engine.
13861x = segLen(seg1)
13862
13863// Triangle with side length 2*x.
13864sketch(on = XY) {
13865  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13866  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13867  coincident([line1.end, line2.start])
13868  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13869  coincident([line2.end, line3.start])
13870  coincident([line3.end, line1.start])
13871  equalLength([line3, line1])
13872  equalLength([line1, line2])
13873  distance([line1.start, line1.end]) == 2*x
13874}
13875
13876// Line segment with length x.
13877sketch2 = sketch(on = XY) {
13878  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13879  distance([line1.start, line1.end]) == x
13880}
13881";
13882
13883        let program = Program::parse(initial_source).unwrap().0.unwrap();
13884
13885        let mut frontend = FrontendState::new();
13886
13887        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13888        let mock_ctx = ExecutorContext::new_mock(None).await;
13889        let version = Version(0);
13890        let project_id = ProjectId(0);
13891        let file_id = FileId(0);
13892
13893        frontend.hack_set_program(&ctx, program).await.unwrap();
13894        let sketch_objects = frontend
13895            .scene_graph
13896            .objects
13897            .iter()
13898            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13899            .collect::<Vec<_>>();
13900        let sketch1_id = sketch_objects.first().unwrap().id;
13901        let sketch2_id = sketch_objects.get(1).unwrap().id;
13902        // First point in sketch1.
13903        let point1_id = ObjectId(sketch1_id.0 + 1);
13904        // First point in sketch2.
13905        let point2_id = ObjectId(sketch2_id.0 + 1);
13906
13907        // Edit the first sketch. Objects before the sketch block should be
13908        // present from execution cache so that we can sketch on prior planes,
13909        // for example. Objects after the first sketch block should not be
13910        // present since those statements are skipped in sketch mode.
13911        //
13912        // - startSketchOn(XY) Plane 1
13913        // - sketch on=XY Plane 1
13914        // - Sketch block 16
13915        let scene_delta = frontend
13916            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13917            .await
13918            .unwrap();
13919        assert_eq!(
13920            scene_delta.new_graph.objects.len(),
13921            18,
13922            "{:#?}",
13923            scene_delta.new_graph.objects
13924        );
13925
13926        // Edit a point in the first sketch.
13927        let point_ctor = PointCtor {
13928            position: Point2d {
13929                x: Expr::Var(Number {
13930                    value: 1.0,
13931                    units: NumericSuffix::Mm,
13932                }),
13933                y: Expr::Var(Number {
13934                    value: 2.0,
13935                    units: NumericSuffix::Mm,
13936                }),
13937            },
13938        };
13939        let segments = vec![ExistingSegmentCtor {
13940            id: point1_id,
13941            ctor: SegmentCtor::Point(point_ctor),
13942        }];
13943        let (src_delta, _) = frontend
13944            .edit_segments(&mock_ctx, version, sketch1_id, segments)
13945            .await
13946            .unwrap();
13947        // Only the first sketch block changes.
13948        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
13949        let edited_sketch1_source = src_delta.text.clone();
13950
13951        // Execute mock to simulate drag end.
13952        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
13953        assert_eq!(src_delta.text, edited_sketch1_source);
13954        // Exit sketch. Objects from the entire program should be present.
13955        //
13956        // - startSketchOn(XY) Plane 1
13957        // - sketch on=XY Plane 1
13958        // - Sketch block 16
13959        // - sketch on=XY cached
13960        // - Sketch block 5
13961        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
13962        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
13963
13964        // Edit the second sketch.
13965        //
13966        // - startSketchOn(XY) Plane 1
13967        // - sketch on=XY Plane 1
13968        // - Sketch block 16
13969        // - sketch on=XY cached
13970        // - Sketch block 5
13971        let scene_delta = frontend
13972            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
13973            .await
13974            .unwrap();
13975        assert_eq!(
13976            scene_delta.new_graph.objects.len(),
13977            24,
13978            "{:#?}",
13979            scene_delta.new_graph.objects
13980        );
13981
13982        // Edit a point in the second sketch.
13983        let point_ctor = PointCtor {
13984            position: Point2d {
13985                x: Expr::Var(Number {
13986                    value: 3.0,
13987                    units: NumericSuffix::Mm,
13988                }),
13989                y: Expr::Var(Number {
13990                    value: 4.0,
13991                    units: NumericSuffix::Mm,
13992                }),
13993            },
13994        };
13995        let segments = vec![ExistingSegmentCtor {
13996            id: point2_id,
13997            ctor: SegmentCtor::Point(point_ctor),
13998        }];
13999        let (src_delta, _) = frontend
14000            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14001            .await
14002            .unwrap();
14003        // Only the second sketch block changes.
14004        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
14005        let edited_sketch2_source = src_delta.text.clone();
14006
14007        // Execute mock to simulate drag end.
14008        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14009        assert_eq!(src_delta.text, edited_sketch2_source);
14010
14011        ctx.close().await;
14012        mock_ctx.close().await;
14013    }
14014
14015    #[tokio::test(flavor = "multi_thread")]
14016    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14017        clear_mem_cache().await;
14018
14019        let source = r#"sketch001 = sketch(on = XZ) {
14020  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14021}
14022sketch002 = sketch(on = XY) {
14023  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14024  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14025  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14026  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14027  coincident([line1.end, line2.start])
14028  coincident([line2.end, line3.start])
14029  coincident([line3.end, line4.start])
14030  coincident([line4.end, line1.start])
14031  parallel([line2, line4])
14032  parallel([line3, line1])
14033  perpendicular([line1, line2])
14034  horizontal(line3)
14035  coincident([line1.start, ORIGIN])
14036}
14037"#;
14038
14039        let program = Program::parse(source).unwrap().0.unwrap();
14040        let mut frontend = FrontendState::new();
14041        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14042        let mock_ctx = ExecutorContext::new_mock(None).await;
14043        let version = Version(0);
14044        let project_id = ProjectId(0);
14045        let file_id = FileId(0);
14046
14047        frontend.hack_set_program(&ctx, program).await.unwrap();
14048        let sketch_objects = frontend
14049            .scene_graph
14050            .objects
14051            .iter()
14052            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14053            .collect::<Vec<_>>();
14054        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14055
14056        let sketch1_id = sketch_objects[0].id;
14057        let sketch2_id = sketch_objects[1].id;
14058
14059        frontend
14060            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14061            .await
14062            .unwrap();
14063        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14064
14065        let scene_delta = frontend
14066            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14067            .await
14068            .unwrap();
14069        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14070
14071        clear_mem_cache().await;
14072        ctx.close().await;
14073        mock_ctx.close().await;
14074    }
14075
14076    // Regression tests: operations on source code with extra whitespace/newlines.
14077    // These test that NodePath-based lookups work correctly when source ranges
14078    // are shifted by extra whitespace that wouldn't be present after formatting.
14079
14080    #[tokio::test(flavor = "multi_thread")]
14081    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14082        // Extra newlines after @settings line - this shifts all source ranges.
14083        let initial_source = "@settings(defaultLengthUnit = mm)
14084
14085sketch001 = sketch(on = XY) {
14086  point(at = [1in, 2in])
14087}
14088";
14089
14090        let program = Program::parse(initial_source).unwrap().0.unwrap();
14091        let mut frontend = FrontendState::new();
14092
14093        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14094        let mock_ctx = ExecutorContext::new_mock(None).await;
14095        let version = Version(0);
14096        let project_id = ProjectId(0);
14097        let file_id = FileId(0);
14098
14099        frontend.hack_set_program(&ctx, program).await.unwrap();
14100        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14101        let sketch_id = sketch_object.id;
14102
14103        // Edit sketch should succeed despite extra newlines.
14104        frontend
14105            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14106            .await
14107            .unwrap();
14108
14109        // Add a new point to the sketch.
14110        let point_ctor = PointCtor {
14111            position: Point2d {
14112                x: Expr::Number(Number {
14113                    value: 5.0,
14114                    units: NumericSuffix::Mm,
14115                }),
14116                y: Expr::Number(Number {
14117                    value: 6.0,
14118                    units: NumericSuffix::Mm,
14119                }),
14120            },
14121        };
14122        let segment = SegmentCtor::Point(point_ctor);
14123        let (src_delta, scene_delta) = frontend
14124            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14125            .await
14126            .unwrap();
14127        // After adding a point, the source should be reformatted with standard whitespace.
14128        assert!(
14129            src_delta.text.contains("point(at = [5mm, 6mm])"),
14130            "Expected new point in source, got: {}",
14131            src_delta.text
14132        );
14133        assert!(!scene_delta.new_objects.is_empty());
14134
14135        ctx.close().await;
14136        mock_ctx.close().await;
14137    }
14138
14139    #[tokio::test(flavor = "multi_thread")]
14140    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14141        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14142
14143        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14144        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14145
14146        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14147        assert!(
14148            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14149            "Expected experimental settings to be added to source"
14150        );
14151    }
14152
14153    #[tokio::test(flavor = "multi_thread")]
14154    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14155        // Extra newlines after @settings, with an empty sketch block.
14156        let initial_source = "@settings(defaultLengthUnit = mm)
14157
14158s = sketch(on = XY) {}
14159";
14160
14161        let program = Program::parse(initial_source).unwrap().0.unwrap();
14162        let mut frontend = FrontendState::new();
14163
14164        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14165        let mock_ctx = ExecutorContext::new_mock(None).await;
14166        let version = Version(0);
14167
14168        frontend.hack_set_program(&ctx, program).await.unwrap();
14169        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14170        let sketch_id = sketch_object.id;
14171
14172        let line_ctor = LineCtor {
14173            start: Point2d {
14174                x: Expr::Number(Number {
14175                    value: 0.0,
14176                    units: NumericSuffix::Mm,
14177                }),
14178                y: Expr::Number(Number {
14179                    value: 0.0,
14180                    units: NumericSuffix::Mm,
14181                }),
14182            },
14183            end: Point2d {
14184                x: Expr::Number(Number {
14185                    value: 10.0,
14186                    units: NumericSuffix::Mm,
14187                }),
14188                y: Expr::Number(Number {
14189                    value: 10.0,
14190                    units: NumericSuffix::Mm,
14191                }),
14192            },
14193            construction: None,
14194        };
14195        let segment = SegmentCtor::Line(line_ctor);
14196        let (src_delta, scene_delta) = frontend
14197            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14198            .await
14199            .unwrap();
14200        assert!(
14201            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14202            "Expected line in source, got: {}",
14203            src_delta.text
14204        );
14205        // Line creates start point, end point, and line segment.
14206        assert_eq!(scene_delta.new_objects.len(), 3);
14207
14208        ctx.close().await;
14209        mock_ctx.close().await;
14210    }
14211
14212    #[tokio::test(flavor = "multi_thread")]
14213    async fn test_extra_newlines_between_operations_edit_line() {
14214        // Extra newlines between @settings and sketch, and inside the sketch block.
14215        let initial_source = "@settings(defaultLengthUnit = mm)
14216
14217sketch001 = sketch(on = XY) {
14218
14219  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14220
14221}
14222";
14223
14224        let program = Program::parse(initial_source).unwrap().0.unwrap();
14225        let mut frontend = FrontendState::new();
14226
14227        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14228        let mock_ctx = ExecutorContext::new_mock(None).await;
14229        let version = Version(0);
14230        let project_id = ProjectId(0);
14231        let file_id = FileId(0);
14232
14233        frontend.hack_set_program(&ctx, program).await.unwrap();
14234        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14235        let sketch_id = sketch_object.id;
14236        let sketch = expect_sketch(sketch_object);
14237
14238        // Extract segment IDs before edit_sketch borrows frontend mutably.
14239        let line_id = sketch
14240            .segments
14241            .iter()
14242            .copied()
14243            .find(|seg_id| {
14244                matches!(
14245                    &frontend.scene_graph.objects[seg_id.0].kind,
14246                    ObjectKind::Segment {
14247                        segment: Segment::Line(_)
14248                    }
14249                )
14250            })
14251            .expect("Expected a line segment in sketch");
14252
14253        // Enter sketch edit mode.
14254        frontend
14255            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14256            .await
14257            .unwrap();
14258
14259        // Edit the line.
14260        let line_ctor = LineCtor {
14261            start: Point2d {
14262                x: Expr::Var(Number {
14263                    value: 1.0,
14264                    units: NumericSuffix::Mm,
14265                }),
14266                y: Expr::Var(Number {
14267                    value: 2.0,
14268                    units: NumericSuffix::Mm,
14269                }),
14270            },
14271            end: Point2d {
14272                x: Expr::Var(Number {
14273                    value: 13.0,
14274                    units: NumericSuffix::Mm,
14275                }),
14276                y: Expr::Var(Number {
14277                    value: 14.0,
14278                    units: NumericSuffix::Mm,
14279                }),
14280            },
14281            construction: None,
14282        };
14283        let segments = vec![ExistingSegmentCtor {
14284            id: line_id,
14285            ctor: SegmentCtor::Line(line_ctor),
14286        }];
14287        let (src_delta, _scene_delta) = frontend
14288            .edit_segments(&mock_ctx, version, sketch_id, segments)
14289            .await
14290            .unwrap();
14291        assert!(
14292            src_delta
14293                .text
14294                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14295            "Expected edited line in source, got: {}",
14296            src_delta.text
14297        );
14298
14299        ctx.close().await;
14300        mock_ctx.close().await;
14301    }
14302
14303    #[tokio::test(flavor = "multi_thread")]
14304    async fn test_extra_newlines_delete_segment() {
14305        // Extra whitespace before and after the sketch block.
14306        let initial_source = "@settings(defaultLengthUnit = mm)
14307
14308sketch001 = sketch(on = XY) {
14309  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14310}
14311";
14312
14313        let program = Program::parse(initial_source).unwrap().0.unwrap();
14314        let mut frontend = FrontendState::new();
14315
14316        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14317        let mock_ctx = ExecutorContext::new_mock(None).await;
14318        let version = Version(0);
14319
14320        frontend.hack_set_program(&ctx, program).await.unwrap();
14321        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14322        let sketch_id = sketch_object.id;
14323        let sketch = expect_sketch(sketch_object);
14324
14325        // The sketch should have 3 segments: start point, center point, and the circle.
14326        assert_eq!(sketch.segments.len(), 3);
14327        let circle_id = sketch.segments[2];
14328
14329        // Delete the circle despite extra newlines in original source.
14330        let (src_delta, scene_delta) = frontend
14331            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14332            .await
14333            .unwrap();
14334        assert!(
14335            src_delta.text.contains("sketch(on = XY) {"),
14336            "Expected sketch block in source, got: {}",
14337            src_delta.text
14338        );
14339        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14340        let new_sketch = expect_sketch(new_sketch_object);
14341        assert_eq!(new_sketch.segments.len(), 0);
14342
14343        ctx.close().await;
14344        mock_ctx.close().await;
14345    }
14346
14347    #[tokio::test(flavor = "multi_thread")]
14348    async fn test_unformatted_source_add_arc() {
14349        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14350        let initial_source = "@settings(defaultLengthUnit = mm)
14351
14352sketch001 = sketch(on = XY) {
14353}
14354";
14355
14356        let program = Program::parse(initial_source).unwrap().0.unwrap();
14357        let mut frontend = FrontendState::new();
14358
14359        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14360        let mock_ctx = ExecutorContext::new_mock(None).await;
14361        let version = Version(0);
14362
14363        frontend.hack_set_program(&ctx, program).await.unwrap();
14364        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14365        let sketch_id = sketch_object.id;
14366
14367        let arc_ctor = ArcCtor {
14368            start: Point2d {
14369                x: Expr::Var(Number {
14370                    value: 5.0,
14371                    units: NumericSuffix::Mm,
14372                }),
14373                y: Expr::Var(Number {
14374                    value: 0.0,
14375                    units: NumericSuffix::Mm,
14376                }),
14377            },
14378            end: Point2d {
14379                x: Expr::Var(Number {
14380                    value: 0.0,
14381                    units: NumericSuffix::Mm,
14382                }),
14383                y: Expr::Var(Number {
14384                    value: 5.0,
14385                    units: NumericSuffix::Mm,
14386                }),
14387            },
14388            center: Point2d {
14389                x: Expr::Var(Number {
14390                    value: 0.0,
14391                    units: NumericSuffix::Mm,
14392                }),
14393                y: Expr::Var(Number {
14394                    value: 0.0,
14395                    units: NumericSuffix::Mm,
14396                }),
14397            },
14398            construction: None,
14399        };
14400        let segment = SegmentCtor::Arc(arc_ctor);
14401        let (src_delta, scene_delta) = frontend
14402            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14403            .await
14404            .unwrap();
14405        assert!(
14406            src_delta
14407                .text
14408                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14409            "Expected arc in source, got: {}",
14410            src_delta.text
14411        );
14412        assert!(!scene_delta.new_objects.is_empty());
14413
14414        ctx.close().await;
14415        mock_ctx.close().await;
14416    }
14417
14418    #[tokio::test(flavor = "multi_thread")]
14419    async fn test_extra_newlines_add_circle() {
14420        // Extra blank lines between settings and sketch.
14421        let initial_source = "@settings(defaultLengthUnit = mm)
14422
14423sketch001 = sketch(on = XY) {
14424}
14425";
14426
14427        let program = Program::parse(initial_source).unwrap().0.unwrap();
14428        let mut frontend = FrontendState::new();
14429
14430        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14431        let mock_ctx = ExecutorContext::new_mock(None).await;
14432        let version = Version(0);
14433
14434        frontend.hack_set_program(&ctx, program).await.unwrap();
14435        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14436        let sketch_id = sketch_object.id;
14437
14438        let circle_ctor = CircleCtor {
14439            start: Point2d {
14440                x: Expr::Var(Number {
14441                    value: 5.0,
14442                    units: NumericSuffix::Mm,
14443                }),
14444                y: Expr::Var(Number {
14445                    value: 0.0,
14446                    units: NumericSuffix::Mm,
14447                }),
14448            },
14449            center: Point2d {
14450                x: Expr::Var(Number {
14451                    value: 0.0,
14452                    units: NumericSuffix::Mm,
14453                }),
14454                y: Expr::Var(Number {
14455                    value: 0.0,
14456                    units: NumericSuffix::Mm,
14457                }),
14458            },
14459            construction: None,
14460        };
14461        let segment = SegmentCtor::Circle(circle_ctor);
14462        let (src_delta, scene_delta) = frontend
14463            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14464            .await
14465            .unwrap();
14466        assert!(
14467            src_delta
14468                .text
14469                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14470            "Expected circle in source, got: {}",
14471            src_delta.text
14472        );
14473        assert!(!scene_delta.new_objects.is_empty());
14474
14475        ctx.close().await;
14476        mock_ctx.close().await;
14477    }
14478
14479    #[tokio::test(flavor = "multi_thread")]
14480    async fn test_extra_newlines_add_constraint() {
14481        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14482        let initial_source = "@settings(defaultLengthUnit = mm)
14483
14484sketch001 = sketch(on = XY) {
14485  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14486  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14487}
14488";
14489
14490        let program = Program::parse(initial_source).unwrap().0.unwrap();
14491        let mut frontend = FrontendState::new();
14492
14493        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14494        let mock_ctx = ExecutorContext::new_mock(None).await;
14495        let version = Version(0);
14496        let project_id = ProjectId(0);
14497        let file_id = FileId(0);
14498
14499        frontend.hack_set_program(&ctx, program).await.unwrap();
14500        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14501        let sketch_id = sketch_object.id;
14502        let sketch = expect_sketch(sketch_object);
14503
14504        // Extract segment data before edit_sketch borrows frontend mutably.
14505        let line_ids: Vec<ObjectId> = sketch
14506            .segments
14507            .iter()
14508            .copied()
14509            .filter(|seg_id| {
14510                matches!(
14511                    &frontend.scene_graph.objects[seg_id.0].kind,
14512                    ObjectKind::Segment {
14513                        segment: Segment::Line(_)
14514                    }
14515                )
14516            })
14517            .collect();
14518        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14519
14520        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14521        let ObjectKind::Segment {
14522            segment: Segment::Line(line1_data),
14523        } = &line1.kind
14524        else {
14525            panic!("Expected line");
14526        };
14527        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14528        let ObjectKind::Segment {
14529            segment: Segment::Line(line2_data),
14530        } = &line2.kind
14531        else {
14532            panic!("Expected line");
14533        };
14534
14535        // Build constraint before entering sketch mode.
14536        let constraint = Constraint::Coincident(Coincident {
14537            segments: vec![line1_data.end.into(), line2_data.start.into()],
14538        });
14539
14540        // Enter sketch edit mode.
14541        frontend
14542            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14543            .await
14544            .unwrap();
14545        let (src_delta, _scene_delta) = frontend
14546            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14547            .await
14548            .unwrap();
14549        assert!(
14550            src_delta.text.contains("coincident("),
14551            "Expected coincident constraint in source, got: {}",
14552            src_delta.text
14553        );
14554
14555        ctx.close().await;
14556        mock_ctx.close().await;
14557    }
14558
14559    #[tokio::test(flavor = "multi_thread")]
14560    async fn test_extra_newlines_add_line_then_edit_line() {
14561        // Extra newlines after @settings - add a line, then edit it.
14562        let initial_source = "@settings(defaultLengthUnit = mm)
14563
14564sketch001 = sketch(on = XY) {
14565}
14566";
14567
14568        let program = Program::parse(initial_source).unwrap().0.unwrap();
14569        let mut frontend = FrontendState::new();
14570
14571        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14572        let mock_ctx = ExecutorContext::new_mock(None).await;
14573        let version = Version(0);
14574
14575        frontend.hack_set_program(&ctx, program).await.unwrap();
14576        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14577        let sketch_id = sketch_object.id;
14578
14579        // Add a line.
14580        let line_ctor = LineCtor {
14581            start: Point2d {
14582                x: Expr::Number(Number {
14583                    value: 0.0,
14584                    units: NumericSuffix::Mm,
14585                }),
14586                y: Expr::Number(Number {
14587                    value: 0.0,
14588                    units: NumericSuffix::Mm,
14589                }),
14590            },
14591            end: Point2d {
14592                x: Expr::Number(Number {
14593                    value: 10.0,
14594                    units: NumericSuffix::Mm,
14595                }),
14596                y: Expr::Number(Number {
14597                    value: 10.0,
14598                    units: NumericSuffix::Mm,
14599                }),
14600            },
14601            construction: None,
14602        };
14603        let segment = SegmentCtor::Line(line_ctor);
14604        let (src_delta, scene_delta) = frontend
14605            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14606            .await
14607            .unwrap();
14608        assert!(
14609            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14610            "Expected line in source after add, got: {}",
14611            src_delta.text
14612        );
14613        // Line creates start point, end point, and line segment.
14614        let line_id = *scene_delta.new_objects.last().unwrap();
14615
14616        // Edit the line.
14617        let line_ctor = LineCtor {
14618            start: Point2d {
14619                x: Expr::Number(Number {
14620                    value: 1.0,
14621                    units: NumericSuffix::Mm,
14622                }),
14623                y: Expr::Number(Number {
14624                    value: 2.0,
14625                    units: NumericSuffix::Mm,
14626                }),
14627            },
14628            end: Point2d {
14629                x: Expr::Number(Number {
14630                    value: 13.0,
14631                    units: NumericSuffix::Mm,
14632                }),
14633                y: Expr::Number(Number {
14634                    value: 14.0,
14635                    units: NumericSuffix::Mm,
14636                }),
14637            },
14638            construction: None,
14639        };
14640        let segments = vec![ExistingSegmentCtor {
14641            id: line_id,
14642            ctor: SegmentCtor::Line(line_ctor),
14643        }];
14644        let (src_delta, scene_delta) = frontend
14645            .edit_segments(&mock_ctx, version, sketch_id, segments)
14646            .await
14647            .unwrap();
14648        assert!(
14649            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14650            "Expected edited line in source, got: {}",
14651            src_delta.text
14652        );
14653        assert_eq!(scene_delta.new_objects, vec![]);
14654
14655        ctx.close().await;
14656        mock_ctx.close().await;
14657    }
14658}