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_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
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::CapSubType;
23use crate::execution::MockConfig;
24use crate::execution::SKETCH_BLOCK_PARAM_ON;
25use crate::execution::annotations::WarningLevel;
26use crate::execution::cache::SketchModeState;
27use crate::execution::cache::clear_mem_cache;
28use crate::execution::cache::read_old_memory;
29use crate::execution::cache::write_old_memory;
30use crate::execution::types::adjust_length;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::ControlPointSplineCtor;
36use crate::front::Distance;
37use crate::front::EqualRadius;
38use crate::front::Error;
39use crate::front::ExecResult;
40use crate::front::FixedPoint;
41use crate::front::Freedom;
42use crate::front::LinesEqualLength;
43use crate::front::Midpoint;
44use crate::front::Object;
45use crate::front::Parallel;
46use crate::front::Perpendicular;
47use crate::front::PointCtor;
48use crate::front::Symmetric;
49use crate::front::Tangent;
50use crate::frontend::api::Expr;
51use crate::frontend::api::FileId;
52use crate::frontend::api::Number;
53use crate::frontend::api::ObjectId;
54use crate::frontend::api::ObjectKind;
55use crate::frontend::api::Plane;
56use crate::frontend::api::ProjectId;
57use crate::frontend::api::RestoreSketchCheckpointOutcome;
58use crate::frontend::api::SceneGraph;
59use crate::frontend::api::SceneGraphDelta;
60use crate::frontend::api::SketchCheckpointId;
61use crate::frontend::api::SourceDelta;
62use crate::frontend::api::SourceRef;
63use crate::frontend::api::Version;
64use crate::frontend::modify::find_defined_names;
65use crate::frontend::modify::next_free_name;
66use crate::frontend::modify::next_free_name_with_padding;
67use crate::frontend::sketch::Coincident;
68use crate::frontend::sketch::Constraint;
69use crate::frontend::sketch::ConstraintSegment;
70use crate::frontend::sketch::Diameter;
71use crate::frontend::sketch::ExistingSegmentCtor;
72use crate::frontend::sketch::Horizontal;
73use crate::frontend::sketch::LineCtor;
74use crate::frontend::sketch::Point2d;
75use crate::frontend::sketch::Radius;
76use crate::frontend::sketch::Segment;
77use crate::frontend::sketch::SegmentCtor;
78use crate::frontend::sketch::SketchApi;
79use crate::frontend::sketch::SketchCtor;
80use crate::frontend::sketch::Vertical;
81use crate::frontend::traverse::MutateBodyItem;
82use crate::frontend::traverse::TraversalReturn;
83use crate::frontend::traverse::Visitor;
84use crate::frontend::traverse::dfs_mut;
85use crate::id::IncIdGenerator;
86use crate::parsing::ast::types as ast;
87use crate::pretty::NumericSuffix;
88use crate::std::constraints::LinesAtAngleKind;
89use crate::walk::NodeMut;
90use crate::walk::Visitable;
91
92pub(crate) mod api;
93pub(crate) mod modify;
94pub(crate) mod sketch;
95
96pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
97
98#[derive(Debug, Clone)]
99struct SketchCheckpoint {
100    id: SketchCheckpointId,
101    source: SourceDelta,
102    program: Program,
103    scene_graph: SceneGraph,
104    exec_outcome: ExecOutcome,
105    point_freedom_cache: HashMap<ObjectId, Freedom>,
106    mock_memory: Option<SketchModeState>,
107}
108mod traverse;
109pub(crate) mod trim;
110
111struct ArcSizeConstraintParams {
112    points: Vec<ObjectId>,
113    function_name: &'static str,
114    value: f64,
115    units: NumericSuffix,
116    label_position: Option<Point2d<Number>>,
117    constraint_type_name: &'static str,
118}
119
120const POINT_FN: &str = "point";
121const POINT_AT_PARAM: &str = "at";
122const LINE_FN: &str = "line";
123const LINE_VARIABLE: &str = "line";
124const LINE_START_PARAM: &str = "start";
125const LINE_END_PARAM: &str = "end";
126const ARC_FN: &str = "arc";
127const ARC_VARIABLE: &str = "arc";
128const ARC_START_PARAM: &str = "start";
129const ARC_END_PARAM: &str = "end";
130const ARC_CENTER_PARAM: &str = "center";
131const CIRCLE_FN: &str = "circle";
132const CIRCLE_VARIABLE: &str = "circle";
133const CIRCLE_START_PARAM: &str = "start";
134const CIRCLE_CENTER_PARAM: &str = "center";
135const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
136const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
137const LABEL_POSITION_PARAM: &str = "labelPosition";
138
139const COINCIDENT_FN: &str = "coincident";
140const DIAMETER_FN: &str = "diameter";
141const DISTANCE_FN: &str = "distance";
142const FIXED_FN: &str = "fixed";
143const ANGLE_FN: &str = "angle";
144const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
145const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
146const EQUAL_LENGTH_FN: &str = "equalLength";
147const EQUAL_RADIUS_FN: &str = "equalRadius";
148const HORIZONTAL_FN: &str = "horizontal";
149const MIDPOINT_FN: &str = "midpoint";
150const MIDPOINT_POINT_PARAM: &str = "point";
151const RADIUS_FN: &str = "radius";
152const SYMMETRIC_FN: &str = "symmetric";
153const SYMMETRIC_AXIS_PARAM: &str = "axis";
154const TANGENT_FN: &str = "tangent";
155const VERTICAL_FN: &str = "vertical";
156
157const LINE_PROPERTY_START: &str = "start";
158const LINE_PROPERTY_END: &str = "end";
159
160const ARC_PROPERTY_START: &str = "start";
161const ARC_PROPERTY_END: &str = "end";
162const ARC_PROPERTY_CENTER: &str = "center";
163const CIRCLE_PROPERTY_START: &str = "start";
164const CIRCLE_PROPERTY_CENTER: &str = "center";
165const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
166const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
167
168const CONSTRUCTION_PARAM: &str = "construction";
169
170#[derive(Debug, Clone, Copy)]
171enum EditDeleteKind {
172    Edit,
173    DeleteNonSketch,
174}
175
176/// Options that control how an edit is re-executed and written back.
177struct ExecuteAfterEditOptions {
178    segment_ids_edited: AhashIndexSet<ObjectId>,
179    edit_kind: EditDeleteKind,
180    commit_solved_initial_guesses: bool,
181}
182
183impl EditDeleteKind {
184    /// Returns true if this edit is any type of deletion.
185    fn is_delete(&self) -> bool {
186        match self {
187            EditDeleteKind::Edit => false,
188            EditDeleteKind::DeleteNonSketch => true,
189        }
190    }
191
192    fn to_change_kind(self) -> ChangeKind {
193        match self {
194            EditDeleteKind::Edit => ChangeKind::Edit,
195            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
196        }
197    }
198}
199
200#[derive(Debug, Clone, Copy)]
201enum ChangeKind {
202    Add,
203    Edit,
204    Delete,
205    None,
206}
207
208#[derive(Debug, Clone, Serialize, ts_rs::TS)]
209#[ts(export, export_to = "FrontendApi.ts")]
210#[serde(tag = "type")]
211pub enum SetProgramOutcome {
212    #[serde(rename_all = "camelCase")]
213    Success {
214        scene_graph: Box<SceneGraph>,
215        exec_outcome: Box<ExecOutcome>,
216        checkpoint_id: Option<SketchCheckpointId>,
217    },
218    #[serde(rename_all = "camelCase")]
219    ExecFailure { error: Box<KclErrorWithOutputs> },
220}
221
222/// Options for a sketch segment edit that participates in drag solving.
223pub struct EditSegmentsOptions {
224    /// Narrows which edited scene objects receive temporary fixed constraints.
225    ///
226    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
227    /// disables those fixed constraints, which is useful for semantic edits such
228    /// as toggling construction state.
229    pub anchor_segment_ids: Option<Vec<ObjectId>>,
230    /// Hidden fixed cursor points that the referenced segment bodies must pass
231    /// through during solve.
232    pub drag_anchors: Vec<SegmentDragAnchor>,
233    /// Whether solver-updated initial guesses should be written back to KCL.
234    pub commit_solved_initial_guesses: bool,
235}
236
237/// Options for a distance-constraint label edit during sketch dragging.
238pub struct EditDistanceConstraintLabelPositionOptions {
239    /// Edited scene objects to keep anchored while previewing the label edit.
240    pub anchor_segment_ids: Vec<ObjectId>,
241    /// Whether solver-updated initial guesses should be written back to KCL.
242    pub commit_solved_initial_guesses: bool,
243}
244
245#[derive(Debug, Clone)]
246pub struct FrontendState {
247    program: Program,
248    scene_graph: SceneGraph,
249    /// Stores the last known freedom value for each point object.
250    /// This allows us to preserve freedom values when freedom analysis isn't run.
251    point_freedom_cache: HashMap<ObjectId, Freedom>,
252    /// One-shot drag anchors for the next segment edit. These ids define which
253    /// edited points/segments become temporary fixed constraints during solve.
254    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
255    /// One-shot segment-body drag anchors for the next segment edit. These add
256    /// a temporary solver point on the dragged segment that follows the cursor.
257    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
258    /// One-shot override for whether the next edit commits solver-updated
259    /// initial guesses back into KCL. Drag previews keep this off so only the
260    /// explicit drag edit feeds the next solve.
261    next_edit_commits_solver_solutions: Option<bool>,
262    sketch_checkpoints: VecDeque<SketchCheckpoint>,
263    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
264}
265
266impl Default for FrontendState {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl FrontendState {
273    pub fn new() -> Self {
274        Self {
275            program: Program::empty(),
276            scene_graph: SceneGraph {
277                project: ProjectId(0),
278                file: FileId(0),
279                version: Version(0),
280                objects: Default::default(),
281                settings: Default::default(),
282                sketch_mode: Default::default(),
283            },
284            point_freedom_cache: HashMap::new(),
285            next_drag_anchor_segment_ids: None,
286            next_segment_drag_anchors: None,
287            next_edit_commits_solver_solutions: None,
288            sketch_checkpoints: VecDeque::new(),
289            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
290        }
291    }
292
293    /// Get a reference to the scene graph
294    pub fn scene_graph(&self) -> &SceneGraph {
295        &self.scene_graph
296    }
297
298    pub fn default_length_unit(&self) -> UnitLength {
299        self.program
300            .meta_settings()
301            .ok()
302            .flatten()
303            .map(|settings| settings.default_length_units)
304            .unwrap_or(UnitLength::Millimeters)
305    }
306
307    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
308        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
309
310        let checkpoint = SketchCheckpoint {
311            id: checkpoint_id,
312            source: SourceDelta {
313                text: source_from_ast(&self.program.ast),
314            },
315            program: self.program.clone(),
316            scene_graph: self.scene_graph.clone(),
317            exec_outcome,
318            point_freedom_cache: self.point_freedom_cache.clone(),
319            mock_memory: read_old_memory().await,
320        };
321
322        self.sketch_checkpoints.push_back(checkpoint);
323        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
324            self.sketch_checkpoints.pop_front();
325        }
326
327        Ok(checkpoint_id)
328    }
329
330    /// Edit sketch segments with optional drag-solve overrides.
331    ///
332    /// Drag anchors add hidden fixed cursor points and constrain the referenced
333    /// segment bodies to pass through them, which lets body drags use the same
334    /// anchor model without pinning all child points. Preview callers disable
335    /// solver writeback so solved geometry can be returned without feeding every
336    /// solver value back into KCL.
337    pub async fn edit_segments_with_options(
338        &mut self,
339        ctx: &ExecutorContext,
340        version: Version,
341        sketch: ObjectId,
342        segments: Vec<ExistingSegmentCtor>,
343        options: EditSegmentsOptions,
344    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
345        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
346            self.next_drag_anchor_segment_ids
347                .replace(anchor_ids.into_iter().collect())
348        });
349        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
350        let previous_commit_mode = self
351            .next_edit_commits_solver_solutions
352            .replace(options.commit_solved_initial_guesses);
353        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
354        if let Some(previous_anchor_ids) = previous_anchor_ids {
355            self.next_drag_anchor_segment_ids = previous_anchor_ids;
356        }
357        self.next_segment_drag_anchors = previous_drag_anchors;
358        self.next_edit_commits_solver_solutions = previous_commit_mode;
359        result
360    }
361
362    /// Edit a distance-constraint label position with optional solver writeback.
363    ///
364    /// Drag previews set `commit_solved_initial_guesses` to false so label
365    /// placement can be previewed against solved geometry without advancing
366    /// persistent KCL state until drag completion.
367    pub async fn edit_distance_constraint_label_position_with_options(
368        &mut self,
369        ctx: &ExecutorContext,
370        version: Version,
371        sketch: ObjectId,
372        constraint_id: ObjectId,
373        label_position: Point2d<Number>,
374        options: EditDistanceConstraintLabelPositionOptions,
375    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
376        let previous_commit_mode = self
377            .next_edit_commits_solver_solutions
378            .replace(options.commit_solved_initial_guesses);
379        let result = SketchApi::edit_distance_constraint_label_position(
380            self,
381            ctx,
382            version,
383            sketch,
384            constraint_id,
385            label_position,
386            options.anchor_segment_ids,
387        )
388        .await;
389        self.next_edit_commits_solver_solutions = previous_commit_mode;
390        result
391    }
392
393    pub async fn restore_sketch_checkpoint(
394        &mut self,
395        checkpoint_id: SketchCheckpointId,
396    ) -> api::Result<RestoreSketchCheckpointOutcome> {
397        let checkpoint = self
398            .sketch_checkpoints
399            .iter()
400            .find(|checkpoint| checkpoint.id == checkpoint_id)
401            .cloned()
402            .ok_or_else(|| Error {
403                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
404            })?;
405
406        self.program = checkpoint.program;
407        self.scene_graph = checkpoint.scene_graph.clone();
408        self.point_freedom_cache = checkpoint.point_freedom_cache;
409        self.next_drag_anchor_segment_ids = None;
410        self.next_segment_drag_anchors = None;
411        self.next_edit_commits_solver_solutions = None;
412
413        if let Some(mock_memory) = checkpoint.mock_memory {
414            write_old_memory(mock_memory).await;
415        } else {
416            clear_mem_cache().await;
417        }
418
419        Ok(RestoreSketchCheckpointOutcome {
420            source_delta: checkpoint.source,
421            scene_graph_delta: SceneGraphDelta {
422                new_graph: self.scene_graph_for_ui(),
423                new_objects: Vec::new(),
424                invalidates_ids: true,
425                exec_outcome: checkpoint.exec_outcome,
426            },
427        })
428    }
429
430    pub fn clear_sketch_checkpoints(&mut self) {
431        self.sketch_checkpoints.clear();
432    }
433    fn scene_graph_for_ui(&self) -> SceneGraph {
434        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
435            matches!(
436                object.kind,
437                ObjectKind::Segment {
438                    segment: Segment::ControlPointSpline(_)
439                }
440            )
441        });
442
443        if !has_control_point_splines {
444            return self.scene_graph.clone();
445        }
446
447        let hidden_constraint_ids = self
448            .scene_graph
449            .objects
450            .iter()
451            .filter_map(|object| match &object.kind {
452                ObjectKind::Constraint {
453                    constraint: Constraint::Coincident(coincident),
454                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
455                    Some(object.id)
456                }
457                _ => None,
458            })
459            .collect::<HashSet<_>>();
460
461        if hidden_constraint_ids.is_empty() {
462            return self.scene_graph.clone();
463        }
464
465        let mut scene_graph = self.scene_graph.clone();
466        for object in &mut scene_graph.objects {
467            match &mut object.kind {
468                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
469                    object.kind = ObjectKind::Nil;
470                }
471                ObjectKind::Sketch(sketch) => {
472                    sketch
473                        .constraints
474                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
475                }
476                _ => {}
477            }
478        }
479
480        scene_graph
481    }
482}
483
484fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
485    let mut first_owner_id = None;
486    for segment_id in coincident.segment_ids() {
487        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
488            return false;
489        };
490
491        match first_owner_id {
492            Some(first_owner_id) if first_owner_id != owner_id => return false,
493            Some(_) => {}
494            None => first_owner_id = Some(owner_id),
495        }
496    }
497
498    first_owner_id.is_some()
499}
500
501fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
502    let object = scene_graph.objects.get(segment_id.0)?;
503    let ObjectKind::Segment { segment } = &object.kind else {
504        return None;
505    };
506
507    match segment {
508        Segment::ControlPointSpline(_) => Some(segment_id),
509        Segment::Point(point) => point
510            .owner
511            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
512        Segment::Line(line) => line
513            .owner
514            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
515        _ => None,
516    }
517}
518
519fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
520    matches!(
521        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
522        Some(ObjectKind::Segment {
523            segment: Segment::ControlPointSpline(_)
524        })
525    )
526}
527
528fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
529    let experimental_features_allowed = program
530        .meta_settings()
531        .ok()
532        .flatten()
533        .map(|settings| settings.experimental_features == WarningLevel::Allow)
534        .unwrap_or(false);
535    if experimental_features_allowed {
536        return Ok(program.clone());
537    }
538
539    program.change_experimental_features(Some(WarningLevel::Allow))
540}
541
542impl SketchApi for FrontendState {
543    async fn execute_mock(
544        &mut self,
545        ctx: &ExecutorContext,
546        _version: Version,
547        sketch: ObjectId,
548    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
549        let sketch_block_ref =
550            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
551
552        let mut truncated_program = self.program.clone();
553        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
554            .map_err(KclErrorWithOutputs::no_outputs)?;
555
556        // Execute.
557        let outcome = ctx
558            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
559            .await?;
560        let new_source = source_from_ast(&self.program.ast);
561        let src_delta = SourceDelta { text: new_source };
562        // MockConfig::default() has freedom_analysis: true
563        let outcome = self.update_state_after_exec(outcome, true);
564        let scene_graph_delta = SceneGraphDelta {
565            new_graph: self.scene_graph.clone(),
566            new_objects: Default::default(),
567            invalidates_ids: false,
568            exec_outcome: outcome,
569        };
570        Ok((src_delta, scene_graph_delta))
571    }
572
573    async fn new_sketch(
574        &mut self,
575        ctx: &ExecutorContext,
576        _project: ProjectId,
577        _file: FileId,
578        _version: Version,
579        args: SketchCtor,
580    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
581        // TODO: Check version.
582
583        let mut new_ast = self.program.ast.clone();
584        // Create updated KCL source from args.
585        let mut plane_ast =
586            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
587        let mut defined_names = find_defined_names(&new_ast);
588        let is_face_of_expr = matches!(
589            &plane_ast,
590            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
591        );
592        if is_face_of_expr {
593            let face_name = next_free_name_with_padding("face", &defined_names)
594                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
595            let face_decl = ast::VariableDeclaration::new(
596                ast::VariableDeclarator::new(&face_name, plane_ast),
597                ast::ItemVisibility::Default,
598                ast::VariableKind::Const,
599            );
600            new_ast
601                .body
602                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
603                    face_decl,
604                ))));
605            defined_names.insert(face_name.clone());
606            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
607        }
608        let sketch_ast = ast::SketchBlock {
609            arguments: vec![ast::LabeledArg {
610                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
611                arg: plane_ast,
612            }],
613            body: Default::default(),
614            is_being_edited: false,
615            non_code_meta: Default::default(),
616            digest: None,
617        };
618        // Add a sketch block as a variable declaration directly, avoiding
619        // source-range mutation on a no-src node.
620        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
621            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
622        let sketch_decl = ast::VariableDeclaration::new(
623            ast::VariableDeclarator::new(
624                &sketch_name,
625                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
626            ),
627            ast::ItemVisibility::Default,
628            ast::VariableKind::Const,
629        );
630        new_ast
631            .body
632            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
633                sketch_decl,
634            ))));
635        // Convert to string source to create real source ranges.
636        let new_source = source_from_ast(&new_ast);
637        // Parse the new source.
638        let (new_program, errors) = Program::parse(&new_source)
639            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
640        if !errors.is_empty() {
641            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
642                "Error parsing KCL source after adding sketch: {errors:?}"
643            ))));
644        }
645        let Some(new_program) = new_program else {
646            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
647                "No AST produced after adding sketch".to_owned(),
648            )));
649        };
650
651        // Make sure to only set this if there are no errors.
652        self.program = new_program.clone();
653
654        // We need to do an engine execute so that the plane object gets created
655        // and is cached.
656        let outcome = ctx.run_with_caching(new_program.clone()).await?;
657        let freedom_analysis_ran = true;
658
659        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
660
661        let Some(sketch_id) = self
662            .scene_graph
663            .objects
664            .iter()
665            .filter_map(|object| match object.kind {
666                ObjectKind::Sketch(_) => Some(object.id),
667                _ => None,
668            })
669            .max_by_key(|id| id.0)
670        else {
671            return Err(KclErrorWithOutputs::from_error_outcome(
672                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
673                outcome,
674            ));
675        };
676        // Store the object in the scene.
677        self.scene_graph.sketch_mode = Some(sketch_id);
678
679        let src_delta = SourceDelta { text: new_source };
680        let scene_graph_delta = SceneGraphDelta {
681            new_graph: self.scene_graph_for_ui(),
682            invalidates_ids: false,
683            new_objects: vec![sketch_id],
684            exec_outcome: outcome,
685        };
686        Ok((src_delta, scene_graph_delta, sketch_id))
687    }
688
689    async fn edit_sketch(
690        &mut self,
691        ctx: &ExecutorContext,
692        _project: ProjectId,
693        _file: FileId,
694        _version: Version,
695        sketch: ObjectId,
696    ) -> ExecResult<SceneGraphDelta> {
697        // TODO: Check version.
698
699        // Look up existing sketch.
700        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
701            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
702        })?;
703        let ObjectKind::Sketch(_) = &sketch_object.kind else {
704            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
705                "Object is not a sketch, it is {}",
706                sketch_object.kind.human_friendly_kind_with_article()
707            ))));
708        };
709        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
710
711        // Enter sketch mode by setting the sketch_mode.
712        self.scene_graph.sketch_mode = Some(sketch);
713
714        // Truncate after the sketch block for mock execution.
715        let mut truncated_program = self.program.clone();
716        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
717            .map_err(KclErrorWithOutputs::no_outputs)?;
718
719        // Execute in mock mode to ensure state is up to date. The caller will
720        // want freedom analysis to display segments correctly.
721        let outcome = ctx
722            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
723            .await?;
724
725        // MockConfig::default() has freedom_analysis: true
726        let outcome = self.update_state_after_exec(outcome, true);
727        let scene_graph_delta = SceneGraphDelta {
728            new_graph: self.scene_graph_for_ui(),
729            invalidates_ids: false,
730            new_objects: Vec::new(),
731            exec_outcome: outcome,
732        };
733        Ok(scene_graph_delta)
734    }
735
736    async fn exit_sketch(
737        &mut self,
738        ctx: &ExecutorContext,
739        _version: Version,
740        sketch: ObjectId,
741    ) -> ExecResult<SceneGraph> {
742        // TODO: Check version.
743        #[cfg(not(target_arch = "wasm32"))]
744        let _ = sketch;
745        #[cfg(target_arch = "wasm32")]
746        if self.scene_graph.sketch_mode != Some(sketch) {
747            web_sys::console::warn_1(
748                &format!(
749                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
750                    &self.scene_graph.sketch_mode
751                )
752                .into(),
753            );
754        }
755        self.scene_graph.sketch_mode = None;
756
757        // Execute.
758        let outcome = ctx.run_with_caching(self.program.clone()).await?;
759
760        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
761        self.update_state_after_exec(outcome, false);
762
763        Ok(self.scene_graph_for_ui())
764    }
765
766    async fn delete_sketch(
767        &mut self,
768        ctx: &ExecutorContext,
769        _version: Version,
770        sketch: ObjectId,
771    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
772        // TODO: Check version.
773
774        let mut new_ast = self.program.ast.clone();
775
776        // Look up existing sketch.
777        let sketch_id = sketch;
778        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
779            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
780        })?;
781        let ObjectKind::Sketch(_) = &sketch_object.kind else {
782            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
783                "Object is not a sketch, it is {}",
784                sketch_object.kind.human_friendly_kind_with_article(),
785            ))));
786        };
787
788        // Modify the AST to remove the sketch.
789        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
790            .map_err(KclErrorWithOutputs::no_outputs)?;
791
792        self.execute_after_delete_sketch(ctx, &mut new_ast).await
793    }
794
795    async fn add_segment(
796        &mut self,
797        ctx: &ExecutorContext,
798        _version: Version,
799        sketch: ObjectId,
800        segment: SegmentCtor,
801        _label: Option<String>,
802    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
803        // TODO: Check version.
804        match segment {
805            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
806            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
807            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
808            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
809            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
810        }
811    }
812
813    async fn edit_segments(
814        &mut self,
815        ctx: &ExecutorContext,
816        _version: Version,
817        sketch: ObjectId,
818        segments: Vec<ExistingSegmentCtor>,
819    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
820        // TODO: Check version.
821        let sketch_block_ref =
822            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
823
824        let mut new_ast = self.program.ast.clone();
825        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
826        let mut invalidates_ids = false;
827
828        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
829        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
830        for segment in &segments {
831            edited_segment_ids.insert(segment.id);
832            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
833                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
834                && let ObjectKind::Segment {
835                    segment: Segment::ControlPointSpline(existing_spline),
836                } = &existing_object.kind
837                && existing_spline.controls.len() != new_ctor.points.len()
838            {
839                invalidates_ids = true;
840            }
841        }
842        let drag_anchor_segment_ids = self
843            .next_drag_anchor_segment_ids
844            .take()
845            .unwrap_or_else(|| edited_segment_ids.clone());
846        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
847
848        // Preprocess segments into a final_edits vector to handle if segments contains:
849        // - edit start point of line1 (as SegmentCtor::Point)
850        // - edit end point of line1 (as SegmentCtor::Point)
851        //
852        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
853        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
854        //
855        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
856        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
857        // so the above example would result in a single line1 edit:
858        // - the first start point edit creates a new line edit entry in final_edits
859        // - the second end point edit finds this line edit and mutates the end position only.
860        //
861        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
862        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
863
864        for segment in segments {
865            let segment_id = segment.id;
866            match segment.ctor {
867                SegmentCtor::Point(ctor) => {
868                    // Find the owner, if any (point -> line / arc)
869                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
870                        && let ObjectKind::Segment { segment } = &segment_object.kind
871                        && let Segment::Point(point) = segment
872                        && let Some(owner_id) = point.owner
873                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
874                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
875                    {
876                        match owner_segment {
877                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
878                                if let Some(existing) = final_edits.get_mut(&owner_id) {
879                                    let SegmentCtor::Line(line_ctor) = existing else {
880                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
881                                            "Internal: Expected line ctor for owner, but found {}",
882                                            existing.human_friendly_kind_with_article()
883                                        ))));
884                                    };
885                                    // Line owner is already in final_edits -> apply this point edit
886                                    if line.start == segment_id {
887                                        line_ctor.start = ctor.position;
888                                    } else {
889                                        line_ctor.end = ctor.position;
890                                    }
891                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
892                                    // Line owner is not in final_edits yet -> create it
893                                    let mut line_ctor = line_ctor.clone();
894                                    if line.start == segment_id {
895                                        line_ctor.start = ctor.position;
896                                    } else {
897                                        line_ctor.end = ctor.position;
898                                    }
899                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
900                                } else {
901                                    // This should never run..
902                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
903                                        "Internal: Line does not have line ctor, but found {}",
904                                        line.ctor.human_friendly_kind_with_article()
905                                    ))));
906                                }
907                                continue;
908                            }
909                            Segment::Arc(arc)
910                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
911                            {
912                                if let Some(existing) = final_edits.get_mut(&owner_id) {
913                                    let SegmentCtor::Arc(arc_ctor) = existing else {
914                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
915                                            "Internal: Expected arc ctor for owner, but found {}",
916                                            existing.human_friendly_kind_with_article()
917                                        ))));
918                                    };
919                                    if arc.start == segment_id {
920                                        arc_ctor.start = ctor.position;
921                                    } else if arc.end == segment_id {
922                                        arc_ctor.end = ctor.position;
923                                    } else {
924                                        arc_ctor.center = ctor.position;
925                                    }
926                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
927                                    let mut arc_ctor = arc_ctor.clone();
928                                    if arc.start == segment_id {
929                                        arc_ctor.start = ctor.position;
930                                    } else if arc.end == segment_id {
931                                        arc_ctor.end = ctor.position;
932                                    } else {
933                                        arc_ctor.center = ctor.position;
934                                    }
935                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
936                                } else {
937                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
938                                        "Internal: Arc does not have arc ctor, but found {}",
939                                        arc.ctor.human_friendly_kind_with_article()
940                                    ))));
941                                }
942                                continue;
943                            }
944                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
945                                if let Some(existing) = final_edits.get_mut(&owner_id) {
946                                    let SegmentCtor::Circle(circle_ctor) = existing else {
947                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
948                                            "Internal: Expected circle ctor for owner, but found {}",
949                                            existing.human_friendly_kind_with_article()
950                                        ))));
951                                    };
952                                    if circle.start == segment_id {
953                                        circle_ctor.start = ctor.position;
954                                    } else {
955                                        circle_ctor.center = ctor.position;
956                                    }
957                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
958                                    let mut circle_ctor = circle_ctor.clone();
959                                    if circle.start == segment_id {
960                                        circle_ctor.start = ctor.position;
961                                    } else {
962                                        circle_ctor.center = ctor.position;
963                                    }
964                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
965                                } else {
966                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
967                                        "Internal: Circle does not have circle ctor, but found {}",
968                                        circle.ctor.human_friendly_kind_with_article()
969                                    ))));
970                                }
971                                continue;
972                            }
973                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
974                                let Some(control_index) =
975                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
976                                else {
977                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
978                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
979                                    ))));
980                                };
981                                if let Some(existing) = final_edits.get_mut(&owner_id) {
982                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
983                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
984                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
985                                            existing.human_friendly_kind_with_article()
986                                        ))));
987                                    };
988                                    spline_ctor.points[control_index] = ctor.position;
989                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
990                                    let mut spline_ctor = spline_ctor.clone();
991                                    spline_ctor.points[control_index] = ctor.position;
992                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
993                                } else {
994                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
995                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
996                                        spline.ctor.human_friendly_kind_with_article()
997                                    ))));
998                                }
999                                continue;
1000                            }
1001                            _ => {}
1002                        }
1003                    }
1004
1005                    // No owner, it's an individual point
1006                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1007                }
1008                SegmentCtor::Line(ctor) => {
1009                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1010                }
1011                SegmentCtor::Arc(ctor) => {
1012                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1013                }
1014                SegmentCtor::Circle(ctor) => {
1015                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1016                }
1017                SegmentCtor::ControlPointSpline(ctor) => {
1018                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1019                }
1020            }
1021        }
1022
1023        for (segment_id, ctor) in final_edits {
1024            match ctor {
1025                SegmentCtor::Point(ctor) => self
1026                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1027                    .map_err(KclErrorWithOutputs::no_outputs)?,
1028                SegmentCtor::Line(ctor) => self
1029                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1030                    .map_err(KclErrorWithOutputs::no_outputs)?,
1031                SegmentCtor::Arc(ctor) => self
1032                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1033                    .map_err(KclErrorWithOutputs::no_outputs)?,
1034                SegmentCtor::Circle(ctor) => self
1035                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1036                    .map_err(KclErrorWithOutputs::no_outputs)?,
1037                SegmentCtor::ControlPointSpline(ctor) => self
1038                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1039                    .map_err(KclErrorWithOutputs::no_outputs)?,
1040            }
1041        }
1042        let (source_delta, mut scene_graph_delta) = self
1043            .execute_after_edit(
1044                ctx,
1045                sketch,
1046                sketch_block_ref,
1047                &mut new_ast,
1048                ExecuteAfterEditOptions {
1049                    segment_ids_edited: drag_anchor_segment_ids,
1050                    edit_kind: EditDeleteKind::Edit,
1051                    commit_solved_initial_guesses,
1052                },
1053            )
1054            .await?;
1055        if invalidates_ids {
1056            scene_graph_delta.invalidates_ids = true;
1057        }
1058        Ok((source_delta, scene_graph_delta))
1059    }
1060
1061    async fn delete_objects(
1062        &mut self,
1063        ctx: &ExecutorContext,
1064        _version: Version,
1065        sketch: ObjectId,
1066        constraint_ids: Vec<ObjectId>,
1067        segment_ids: Vec<ObjectId>,
1068    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1069        // TODO: Check version.
1070        let sketch_block_ref =
1071            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073        // Deduplicate IDs.
1074        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1075        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1076
1077        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1078        // also delete the point, as well as other points that are owned by the owner.
1079        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1080
1081        for segment_id in segment_ids_set.iter().copied() {
1082            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1083                let ObjectKind::Segment { segment } = &segment_object.kind else {
1084                    return None;
1085                };
1086                match segment {
1087                    Segment::Point(point) => point.owner,
1088                    Segment::Line(line) => line.owner,
1089                    _ => None,
1090                }
1091            });
1092
1093            if let Some(owner_id) = owner_id
1094                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1095                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1096                && matches!(
1097                    owner_segment,
1098                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1099                )
1100            {
1101                // segment is owned -> delete the owner
1102                resolved_segment_ids_to_delete.insert(owner_id);
1103            } else {
1104                // segment is not owned by anything -> can be deleted
1105                resolved_segment_ids_to_delete.insert(segment_id);
1106            }
1107        }
1108        let referenced_constraint_ids = self
1109            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1110            .map_err(KclErrorWithOutputs::no_outputs)?;
1111
1112        let mut new_ast = self.program.ast.clone();
1113
1114        for constraint_id in referenced_constraint_ids {
1115            if constraint_ids_set.contains(&constraint_id) {
1116                continue;
1117            }
1118
1119            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1120                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1121            })?;
1122            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1123                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1124                    "Object is not a constraint, it is {}",
1125                    constraint_object.kind.human_friendly_kind_with_article()
1126                ))));
1127            };
1128
1129            match constraint {
1130                Constraint::Coincident(coincident) => {
1131                    let remaining_segments =
1132                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1133
1134                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1135                    if remaining_segments.len() >= 2 {
1136                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1137                            .map_err(KclErrorWithOutputs::no_outputs)?;
1138                    } else {
1139                        constraint_ids_set.insert(constraint_id);
1140                    }
1141                }
1142                Constraint::EqualRadius(equal_radius) => {
1143                    let remaining_input = equal_radius
1144                        .input
1145                        .iter()
1146                        .copied()
1147                        .filter(|segment_id| {
1148                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1149                        })
1150                        .collect::<Vec<_>>();
1151
1152                    if remaining_input.len() >= 2 {
1153                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1154                            .map_err(KclErrorWithOutputs::no_outputs)?;
1155                    } else {
1156                        constraint_ids_set.insert(constraint_id);
1157                    }
1158                }
1159                Constraint::LinesEqualLength(lines_equal_length) => {
1160                    let remaining_lines = lines_equal_length
1161                        .lines
1162                        .iter()
1163                        .copied()
1164                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1165                        .collect::<Vec<_>>();
1166
1167                    // Equal length constraint is only valid with at least 2 lines
1168                    if remaining_lines.len() >= 2 {
1169                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1170                            .map_err(KclErrorWithOutputs::no_outputs)?;
1171                    } else {
1172                        constraint_ids_set.insert(constraint_id);
1173                    }
1174                }
1175                Constraint::Parallel(parallel) => {
1176                    let remaining_lines = parallel
1177                        .lines
1178                        .iter()
1179                        .copied()
1180                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1181                        .collect::<Vec<_>>();
1182
1183                    if remaining_lines.len() >= 2 {
1184                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1185                            .map_err(KclErrorWithOutputs::no_outputs)?;
1186                    } else {
1187                        constraint_ids_set.insert(constraint_id);
1188                    }
1189                }
1190                Constraint::Horizontal(Horizontal::Points { points }) => {
1191                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1192
1193                    if remaining_points.len() >= 2 {
1194                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1195                            .map_err(KclErrorWithOutputs::no_outputs)?;
1196                    } else {
1197                        constraint_ids_set.insert(constraint_id);
1198                    }
1199                }
1200                Constraint::Vertical(Vertical::Points { points }) => {
1201                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1202
1203                    if remaining_points.len() >= 2 {
1204                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1205                            .map_err(KclErrorWithOutputs::no_outputs)?;
1206                    } else {
1207                        constraint_ids_set.insert(constraint_id);
1208                    }
1209                }
1210                Constraint::Fixed(fixed) => {
1211                    if fixed.points.iter().any(|fixed_point| {
1212                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1213                    }) {
1214                        constraint_ids_set.insert(constraint_id);
1215                    }
1216                }
1217                _ => {
1218                    // All other constraint types: if referenced by a segment -> delete the constraint
1219                    constraint_ids_set.insert(constraint_id);
1220                }
1221            }
1222        }
1223
1224        for constraint_id in constraint_ids_set {
1225            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1226                .map_err(KclErrorWithOutputs::no_outputs)?;
1227        }
1228        for segment_id in resolved_segment_ids_to_delete {
1229            self.delete_segment(&mut new_ast, sketch, segment_id)
1230                .map_err(KclErrorWithOutputs::no_outputs)?;
1231        }
1232
1233        self.execute_after_edit(
1234            ctx,
1235            sketch,
1236            sketch_block_ref,
1237            &mut new_ast,
1238            ExecuteAfterEditOptions {
1239                segment_ids_edited: Default::default(),
1240                edit_kind: EditDeleteKind::DeleteNonSketch,
1241                commit_solved_initial_guesses: true,
1242            },
1243        )
1244        .await
1245    }
1246
1247    async fn add_constraint(
1248        &mut self,
1249        ctx: &ExecutorContext,
1250        _version: Version,
1251        sketch: ObjectId,
1252        constraint: Constraint,
1253    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1254        // TODO: Check version.
1255
1256        // Save the original state as a backup - we'll restore it if anything fails
1257        let original_program = self.program.clone();
1258        let original_scene_graph = self.scene_graph.clone();
1259
1260        let mut new_ast = self.program.ast.clone();
1261        let sketch_block_ref = match constraint {
1262            Constraint::Coincident(coincident) => self
1263                .add_coincident(sketch, coincident, &mut new_ast)
1264                .await
1265                .map_err(KclErrorWithOutputs::no_outputs)?,
1266            Constraint::Distance(distance) => self
1267                .add_distance(sketch, distance, &mut new_ast)
1268                .await
1269                .map_err(KclErrorWithOutputs::no_outputs)?,
1270            Constraint::EqualRadius(equal_radius) => self
1271                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1272                .await
1273                .map_err(KclErrorWithOutputs::no_outputs)?,
1274            Constraint::Fixed(fixed) => self
1275                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1276                .await
1277                .map_err(KclErrorWithOutputs::no_outputs)?,
1278            Constraint::HorizontalDistance(distance) => self
1279                .add_horizontal_distance(sketch, distance, &mut new_ast)
1280                .await
1281                .map_err(KclErrorWithOutputs::no_outputs)?,
1282            Constraint::VerticalDistance(distance) => self
1283                .add_vertical_distance(sketch, distance, &mut new_ast)
1284                .await
1285                .map_err(KclErrorWithOutputs::no_outputs)?,
1286            Constraint::Horizontal(horizontal) => self
1287                .add_horizontal(sketch, horizontal, &mut new_ast)
1288                .await
1289                .map_err(KclErrorWithOutputs::no_outputs)?,
1290            Constraint::LinesEqualLength(lines_equal_length) => self
1291                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1292                .await
1293                .map_err(KclErrorWithOutputs::no_outputs)?,
1294            Constraint::Midpoint(midpoint) => self
1295                .add_midpoint(sketch, midpoint, &mut new_ast)
1296                .await
1297                .map_err(KclErrorWithOutputs::no_outputs)?,
1298            Constraint::Parallel(parallel) => self
1299                .add_parallel(sketch, parallel, &mut new_ast)
1300                .await
1301                .map_err(KclErrorWithOutputs::no_outputs)?,
1302            Constraint::Perpendicular(perpendicular) => self
1303                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1304                .await
1305                .map_err(KclErrorWithOutputs::no_outputs)?,
1306            Constraint::Radius(radius) => self
1307                .add_radius(sketch, radius, &mut new_ast)
1308                .await
1309                .map_err(KclErrorWithOutputs::no_outputs)?,
1310            Constraint::Diameter(diameter) => self
1311                .add_diameter(sketch, diameter, &mut new_ast)
1312                .await
1313                .map_err(KclErrorWithOutputs::no_outputs)?,
1314            Constraint::Symmetric(symmetric) => self
1315                .add_symmetric(sketch, symmetric, &mut new_ast)
1316                .await
1317                .map_err(KclErrorWithOutputs::no_outputs)?,
1318            Constraint::Vertical(vertical) => self
1319                .add_vertical(sketch, vertical, &mut new_ast)
1320                .await
1321                .map_err(KclErrorWithOutputs::no_outputs)?,
1322            Constraint::Angle(lines_at_angle) => self
1323                .add_angle(sketch, lines_at_angle, &mut new_ast)
1324                .await
1325                .map_err(KclErrorWithOutputs::no_outputs)?,
1326            Constraint::Tangent(tangent) => self
1327                .add_tangent(sketch, tangent, &mut new_ast)
1328                .await
1329                .map_err(KclErrorWithOutputs::no_outputs)?,
1330        };
1331
1332        let result = self
1333            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1334            .await;
1335
1336        // If execution failed, restore the original state to prevent corruption
1337        if result.is_err() {
1338            self.program = original_program;
1339            self.scene_graph = original_scene_graph;
1340        }
1341
1342        result
1343    }
1344
1345    async fn chain_segment(
1346        &mut self,
1347        ctx: &ExecutorContext,
1348        version: Version,
1349        sketch: ObjectId,
1350        previous_segment_end_point_id: ObjectId,
1351        segment: SegmentCtor,
1352        _label: Option<String>,
1353    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1354        // TODO: Check version.
1355
1356        // First, add the segment (line) to get its start point ID
1357        let SegmentCtor::Line(line_ctor) = segment else {
1358            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1359                "chain_segment currently only supports Line segments, got {}",
1360                segment.human_friendly_kind_with_article(),
1361            ))));
1362        };
1363
1364        // Add the line segment first - this updates self.program and self.scene_graph
1365        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1366
1367        // Find the new line's start point ID from the updated scene graph
1368        // add_line updates self.scene_graph, so we can use that
1369        let new_line_id = first_scene_delta
1370            .new_objects
1371            .iter()
1372            .find(|&obj_id| {
1373                let obj = self.scene_graph.objects.get(obj_id.0);
1374                if let Some(obj) = obj {
1375                    matches!(
1376                        &obj.kind,
1377                        ObjectKind::Segment {
1378                            segment: Segment::Line(_)
1379                        }
1380                    )
1381                } else {
1382                    false
1383                }
1384            })
1385            .ok_or_else(|| {
1386                KclErrorWithOutputs::no_outputs(KclError::refactor(
1387                    "Failed to find new line segment in scene graph".to_string(),
1388                ))
1389            })?;
1390
1391        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1392            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1393                "New line object not found: {new_line_id:?}"
1394            )))
1395        })?;
1396
1397        let ObjectKind::Segment {
1398            segment: new_line_segment,
1399        } = &new_line_obj.kind
1400        else {
1401            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1402                "Object is not a segment: {new_line_obj:?}"
1403            ))));
1404        };
1405
1406        let Segment::Line(new_line) = new_line_segment else {
1407            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1408                "Segment is not a line: {new_line_segment:?}"
1409            ))));
1410        };
1411
1412        let new_line_start_point_id = new_line.start;
1413
1414        // Now add the coincident constraint between the previous end point and the new line's start point.
1415        let coincident = Coincident {
1416            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1417        };
1418
1419        let (final_src_delta, final_scene_delta) = self
1420            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1421            .await?;
1422
1423        // Combine new objects from the line addition and the constraint addition.
1424        // Both add_line and add_constraint now populate new_objects correctly.
1425        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1426        combined_new_objects.extend(final_scene_delta.new_objects);
1427
1428        let scene_graph_delta = SceneGraphDelta {
1429            new_graph: self.scene_graph_for_ui(),
1430            invalidates_ids: false,
1431            new_objects: combined_new_objects,
1432            exec_outcome: final_scene_delta.exec_outcome,
1433        };
1434
1435        Ok((final_src_delta, scene_graph_delta))
1436    }
1437
1438    async fn edit_constraint(
1439        &mut self,
1440        ctx: &ExecutorContext,
1441        _version: Version,
1442        sketch: ObjectId,
1443        constraint_id: ObjectId,
1444        value_expression: String,
1445    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1446        // TODO: Check version.
1447        let sketch_block_ref =
1448            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1449
1450        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1451            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1452        })?;
1453        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1454            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1455                "Object is not a constraint: {constraint_id:?}"
1456            ))));
1457        }
1458
1459        let mut new_ast = self.program.ast.clone();
1460
1461        // Parse the expression string into an AST node.
1462        let (parsed, errors) = Program::parse(&value_expression)
1463            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1464        if !errors.is_empty() {
1465            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1466                "Error parsing value expression: {errors:?}"
1467            ))));
1468        }
1469        let mut parsed = parsed.ok_or_else(|| {
1470            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1471        })?;
1472        if parsed.ast.body.is_empty() {
1473            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1474                "Empty value expression".to_string(),
1475            )));
1476        }
1477        let first = parsed.ast.body.remove(0);
1478        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1479            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1480                "Value expression must be a simple expression".to_string(),
1481            )));
1482        };
1483
1484        let new_value: ast::BinaryPart = expr_stmt
1485            .inner
1486            .expression
1487            .try_into()
1488            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1489
1490        self.mutate_ast(
1491            &mut new_ast,
1492            constraint_id,
1493            AstMutateCommand::EditConstraintValue { value: new_value },
1494        )
1495        .map_err(KclErrorWithOutputs::no_outputs)?;
1496
1497        self.execute_after_edit(
1498            ctx,
1499            sketch,
1500            sketch_block_ref,
1501            &mut new_ast,
1502            ExecuteAfterEditOptions {
1503                segment_ids_edited: Default::default(),
1504                edit_kind: EditDeleteKind::Edit,
1505                commit_solved_initial_guesses: true,
1506            },
1507        )
1508        .await
1509    }
1510
1511    async fn edit_distance_constraint_label_position(
1512        &mut self,
1513        ctx: &ExecutorContext,
1514        _version: Version,
1515        sketch: ObjectId,
1516        constraint_id: ObjectId,
1517        label_position: Point2d<Number>,
1518        anchor_segment_ids: Vec<ObjectId>,
1519    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1520        // TODO: Check version.
1521        let sketch_block_ref =
1522            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1523
1524        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1525            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1526        })?;
1527        if !matches!(
1528            &object.kind,
1529            ObjectKind::Constraint {
1530                constraint: Constraint::Distance(_)
1531                    | Constraint::HorizontalDistance(_)
1532                    | Constraint::VerticalDistance(_)
1533                    | Constraint::Radius(_)
1534                    | Constraint::Diameter(_),
1535            }
1536        ) {
1537            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1538                "Object does not support labelPosition: {constraint_id:?}"
1539            ))));
1540        }
1541
1542        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1543            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544                "Could not convert label position to AST: {err}"
1545            )))
1546        })?;
1547        let mut new_ast = self.program.ast.clone();
1548        self.mutate_ast(
1549            &mut new_ast,
1550            constraint_id,
1551            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1552        )
1553        .map_err(KclErrorWithOutputs::no_outputs)?;
1554        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1555
1556        self.execute_after_edit(
1557            ctx,
1558            sketch,
1559            sketch_block_ref,
1560            &mut new_ast,
1561            ExecuteAfterEditOptions {
1562                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1563                edit_kind: EditDeleteKind::Edit,
1564                commit_solved_initial_guesses,
1565            },
1566        )
1567        .await
1568    }
1569
1570    /// Splitting a segment means creating a new segment, editing the old one, and then
1571    /// migrating a bunch of the constraints from the original segment to the new one
1572    /// (i.e. deleting them and re-adding them on the other segment).
1573    ///
1574    /// To keep this efficient we require as few executions as possible: we create the
1575    /// new segment first (to get its id), then do all edits and new constraints, and
1576    /// do all deletes at the end (since deletes invalidate ids).
1577    async fn batch_split_segment_operations(
1578        &mut self,
1579        ctx: &ExecutorContext,
1580        _version: Version,
1581        sketch: ObjectId,
1582        edit_segments: Vec<ExistingSegmentCtor>,
1583        add_constraints: Vec<Constraint>,
1584        delete_constraint_ids: Vec<ObjectId>,
1585        _new_segment_info: sketch::NewSegmentInfo,
1586    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1587        // TODO: Check version.
1588        let sketch_block_ref =
1589            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1590
1591        let mut new_ast = self.program.ast.clone();
1592        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1593
1594        // Step 1: Edit segments
1595        for segment in edit_segments {
1596            segment_ids_edited.insert(segment.id);
1597            match segment.ctor {
1598                SegmentCtor::Point(ctor) => self
1599                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1600                    .map_err(KclErrorWithOutputs::no_outputs)?,
1601                SegmentCtor::Line(ctor) => self
1602                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1603                    .map_err(KclErrorWithOutputs::no_outputs)?,
1604                SegmentCtor::Arc(ctor) => self
1605                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1606                    .map_err(KclErrorWithOutputs::no_outputs)?,
1607                SegmentCtor::Circle(ctor) => self
1608                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1609                    .map_err(KclErrorWithOutputs::no_outputs)?,
1610                SegmentCtor::ControlPointSpline(ctor) => self
1611                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1612                    .map_err(KclErrorWithOutputs::no_outputs)?,
1613            }
1614        }
1615
1616        // Step 2: Add all constraints
1617        for constraint in add_constraints {
1618            match constraint {
1619                Constraint::Coincident(coincident) => {
1620                    self.add_coincident(sketch, coincident, &mut new_ast)
1621                        .await
1622                        .map_err(KclErrorWithOutputs::no_outputs)?;
1623                }
1624                Constraint::Distance(distance) => {
1625                    self.add_distance(sketch, distance, &mut new_ast)
1626                        .await
1627                        .map_err(KclErrorWithOutputs::no_outputs)?;
1628                }
1629                Constraint::EqualRadius(equal_radius) => {
1630                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1631                        .await
1632                        .map_err(KclErrorWithOutputs::no_outputs)?;
1633                }
1634                Constraint::Fixed(fixed) => {
1635                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1636                        .await
1637                        .map_err(KclErrorWithOutputs::no_outputs)?;
1638                }
1639                Constraint::HorizontalDistance(distance) => {
1640                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1641                        .await
1642                        .map_err(KclErrorWithOutputs::no_outputs)?;
1643                }
1644                Constraint::VerticalDistance(distance) => {
1645                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1646                        .await
1647                        .map_err(KclErrorWithOutputs::no_outputs)?;
1648                }
1649                Constraint::Horizontal(horizontal) => {
1650                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1651                        .await
1652                        .map_err(KclErrorWithOutputs::no_outputs)?;
1653                }
1654                Constraint::LinesEqualLength(lines_equal_length) => {
1655                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1656                        .await
1657                        .map_err(KclErrorWithOutputs::no_outputs)?;
1658                }
1659                Constraint::Midpoint(midpoint) => {
1660                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1661                        .await
1662                        .map_err(KclErrorWithOutputs::no_outputs)?;
1663                }
1664                Constraint::Parallel(parallel) => {
1665                    self.add_parallel(sketch, parallel, &mut new_ast)
1666                        .await
1667                        .map_err(KclErrorWithOutputs::no_outputs)?;
1668                }
1669                Constraint::Perpendicular(perpendicular) => {
1670                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1671                        .await
1672                        .map_err(KclErrorWithOutputs::no_outputs)?;
1673                }
1674                Constraint::Vertical(vertical) => {
1675                    self.add_vertical(sketch, vertical, &mut new_ast)
1676                        .await
1677                        .map_err(KclErrorWithOutputs::no_outputs)?;
1678                }
1679                Constraint::Diameter(diameter) => {
1680                    self.add_diameter(sketch, diameter, &mut new_ast)
1681                        .await
1682                        .map_err(KclErrorWithOutputs::no_outputs)?;
1683                }
1684                Constraint::Radius(radius) => {
1685                    self.add_radius(sketch, radius, &mut new_ast)
1686                        .await
1687                        .map_err(KclErrorWithOutputs::no_outputs)?;
1688                }
1689                Constraint::Symmetric(symmetric) => {
1690                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1691                        .await
1692                        .map_err(KclErrorWithOutputs::no_outputs)?;
1693                }
1694                Constraint::Angle(angle) => {
1695                    self.add_angle(sketch, angle, &mut new_ast)
1696                        .await
1697                        .map_err(KclErrorWithOutputs::no_outputs)?;
1698                }
1699                Constraint::Tangent(tangent) => {
1700                    self.add_tangent(sketch, tangent, &mut new_ast)
1701                        .await
1702                        .map_err(KclErrorWithOutputs::no_outputs)?;
1703                }
1704            }
1705        }
1706
1707        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1708        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1709
1710        let has_constraint_deletions = !constraint_ids_set.is_empty();
1711        for constraint_id in constraint_ids_set {
1712            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1713                .map_err(KclErrorWithOutputs::no_outputs)?;
1714        }
1715
1716        // Step 4: Execute once at the end
1717        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1718        // But we'll manually set invalidates_ids: true if we deleted constraints
1719        let (source_delta, mut scene_graph_delta) = self
1720            .execute_after_edit(
1721                ctx,
1722                sketch,
1723                sketch_block_ref,
1724                &mut new_ast,
1725                ExecuteAfterEditOptions {
1726                    segment_ids_edited,
1727                    edit_kind: EditDeleteKind::Edit,
1728                    commit_solved_initial_guesses: true,
1729                },
1730            )
1731            .await?;
1732
1733        // If we deleted constraints, set invalidates_ids: true
1734        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1735        if has_constraint_deletions {
1736            scene_graph_delta.invalidates_ids = true;
1737        }
1738
1739        Ok((source_delta, scene_graph_delta))
1740    }
1741
1742    async fn batch_tail_cut_operations(
1743        &mut self,
1744        ctx: &ExecutorContext,
1745        _version: Version,
1746        sketch: ObjectId,
1747        edit_segments: Vec<ExistingSegmentCtor>,
1748        add_constraints: Vec<Constraint>,
1749        delete_constraint_ids: Vec<ObjectId>,
1750        additional_edited_segment_ids: Vec<ObjectId>,
1751    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1752        let sketch_block_ref =
1753            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1754
1755        let mut new_ast = self.program.ast.clone();
1756        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1757
1758        // Step 1: Edit segments (usually a single segment for tail cut)
1759        for segment in edit_segments {
1760            segment_ids_edited.insert(segment.id);
1761            match segment.ctor {
1762                SegmentCtor::Point(ctor) => self
1763                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1764                    .map_err(KclErrorWithOutputs::no_outputs)?,
1765                SegmentCtor::Line(ctor) => self
1766                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1767                    .map_err(KclErrorWithOutputs::no_outputs)?,
1768                SegmentCtor::Arc(ctor) => self
1769                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1770                    .map_err(KclErrorWithOutputs::no_outputs)?,
1771                SegmentCtor::Circle(ctor) => self
1772                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1773                    .map_err(KclErrorWithOutputs::no_outputs)?,
1774                SegmentCtor::ControlPointSpline(ctor) => self
1775                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1776                    .map_err(KclErrorWithOutputs::no_outputs)?,
1777            }
1778        }
1779
1780        segment_ids_edited.extend(additional_edited_segment_ids);
1781
1782        // Step 2: Add coincident constraints
1783        for constraint in add_constraints {
1784            match constraint {
1785                Constraint::Coincident(coincident) => {
1786                    self.add_coincident(sketch, coincident, &mut new_ast)
1787                        .await
1788                        .map_err(KclErrorWithOutputs::no_outputs)?;
1789                }
1790                other => {
1791                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1792                        "unsupported constraint in tail cut batch: {other:?}"
1793                    ))));
1794                }
1795            }
1796        }
1797
1798        // Step 3: Delete constraints (if any)
1799        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1800
1801        let has_constraint_deletions = !constraint_ids_set.is_empty();
1802        for constraint_id in constraint_ids_set {
1803            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1804                .map_err(KclErrorWithOutputs::no_outputs)?;
1805        }
1806
1807        // Step 4: Single execute_after_edit
1808        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1809        // But we'll manually set invalidates_ids: true if we deleted constraints
1810        let (source_delta, mut scene_graph_delta) = self
1811            .execute_after_edit(
1812                ctx,
1813                sketch,
1814                sketch_block_ref,
1815                &mut new_ast,
1816                ExecuteAfterEditOptions {
1817                    segment_ids_edited,
1818                    edit_kind: EditDeleteKind::Edit,
1819                    commit_solved_initial_guesses: true,
1820                },
1821            )
1822            .await?;
1823
1824        // If we deleted constraints, set invalidates_ids: true
1825        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1826        if has_constraint_deletions {
1827            scene_graph_delta.invalidates_ids = true;
1828        }
1829
1830        Ok((source_delta, scene_graph_delta))
1831    }
1832}
1833
1834impl FrontendState {
1835    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1836        self.program = program.clone();
1837
1838        // Execute so that the objects are updated and available for the next
1839        // API call.
1840        // This always uses engine execution (not mock) so that things are cached.
1841        // Engine execution now runs freedom analysis automatically.
1842        // Keep existing checkpoints alive here. History may still reference
1843        // older committed sketch states across a direct-edit boundary, and a
1844        // checkpoint restore is a full state replacement anyway. We append a
1845        // fresh baseline checkpoint after the full execution below.
1846        // Clear the freedom cache since IDs might have changed after direct editing
1847        // and we're about to run freedom analysis which will repopulate it.
1848        self.point_freedom_cache.clear();
1849        match ctx.run_with_caching(program).await {
1850            Ok(outcome) => {
1851                let outcome = self.update_state_after_exec(outcome, true);
1852                let checkpoint_id = self
1853                    .create_sketch_checkpoint(outcome.clone())
1854                    .await
1855                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1856                Ok(SetProgramOutcome::Success {
1857                    scene_graph: Box::new(self.scene_graph_for_ui()),
1858                    exec_outcome: Box::new(outcome),
1859                    checkpoint_id: Some(checkpoint_id),
1860                })
1861            }
1862            Err(mut err) => {
1863                // Don't return an error just because execution failed. Instead,
1864                // update state as much as possible.
1865                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1866                self.update_state_after_exec(outcome, true);
1867                err.scene_graph = Some(self.scene_graph_for_ui());
1868                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1869            }
1870        }
1871    }
1872
1873    /// Decorate engine execution such that our state is updated and the scene
1874    /// graph is added to the return.
1875    pub async fn engine_execute(
1876        &mut self,
1877        ctx: &ExecutorContext,
1878        program: Program,
1879    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1880        self.program = program.clone();
1881
1882        // Engine execution now runs freedom analysis automatically. Clear the
1883        // freedom cache since IDs might have changed after direct editing, and
1884        // we're about to run freedom analysis which will repopulate it.
1885        self.point_freedom_cache.clear();
1886        match ctx.run_with_caching(program).await {
1887            Ok(outcome) => {
1888                let outcome = self.update_state_after_exec(outcome, true);
1889                Ok(SceneGraphDelta {
1890                    new_graph: self.scene_graph_for_ui(),
1891                    exec_outcome: outcome,
1892                    // We don't know what the new objects are.
1893                    new_objects: Default::default(),
1894                    // We don't know if IDs were invalidated.
1895                    invalidates_ids: Default::default(),
1896                })
1897            }
1898            Err(mut err) => {
1899                // Update state as much as possible, even when there's an error.
1900                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1901                self.update_state_after_exec(outcome, true);
1902                err.scene_graph = Some(self.scene_graph_for_ui());
1903                Err(err)
1904            }
1905        }
1906    }
1907
1908    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1909        if matches!(err.error, KclError::EngineHangup { .. }) {
1910            // It's not ideal to special-case this, but this error is very
1911            // common during development, and it causes confusing downstream
1912            // errors that have nothing to do with the actual problem.
1913            return Err(err);
1914        }
1915
1916        let KclErrorWithOutputs {
1917            error,
1918            mut non_fatal,
1919            variables,
1920            operations,
1921            artifact_graph,
1922            scene_objects,
1923            source_range_to_object,
1924            var_solutions,
1925            filenames,
1926            default_planes,
1927            ..
1928        } = err;
1929
1930        if let Some(source_range) = error.source_ranges().first() {
1931            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1932        } else {
1933            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1934        }
1935
1936        Ok(ExecOutcome {
1937            variables,
1938            filenames,
1939            operations,
1940            artifact_graph,
1941            scene_objects,
1942            source_range_to_object,
1943            var_solutions,
1944            issues: non_fatal,
1945            default_planes,
1946        })
1947    }
1948
1949    async fn add_point(
1950        &mut self,
1951        ctx: &ExecutorContext,
1952        sketch: ObjectId,
1953        ctor: PointCtor,
1954    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1955        // Create updated KCL source from args.
1956        let at_ast = to_ast_point2d(&ctor.position)
1957            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1958        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1959            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1960            unlabeled: None,
1961            arguments: vec![ast::LabeledArg {
1962                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1963                arg: at_ast,
1964            }],
1965            digest: None,
1966            non_code_meta: Default::default(),
1967        })));
1968
1969        // Look up existing sketch.
1970        let sketch_id = sketch;
1971        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1972            #[cfg(target_arch = "wasm32")]
1973            web_sys::console::error_1(
1974                &format!(
1975                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1976                    &self.scene_graph.objects
1977                )
1978                .into(),
1979            );
1980            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1981        })?;
1982        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1983            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1984                "Object is not a sketch, it is {}",
1985                sketch_object.kind.human_friendly_kind_with_article(),
1986            ))));
1987        };
1988        // Add the point to the AST of the sketch block.
1989        let mut new_ast = self.program.ast.clone();
1990        let (sketch_block_ref, _) = self
1991            .mutate_ast(
1992                &mut new_ast,
1993                sketch_id,
1994                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1995            )
1996            .map_err(KclErrorWithOutputs::no_outputs)?;
1997        // Convert to string source to create real source ranges.
1998        let new_source = source_from_ast(&new_ast);
1999        // Parse the new KCL source.
2000        let (new_program, errors) = Program::parse(&new_source)
2001            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2002        if !errors.is_empty() {
2003            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2004                "Error parsing KCL source after adding point: {errors:?}"
2005            ))));
2006        }
2007        let Some(new_program) = new_program else {
2008            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2009                "No AST produced after adding point".to_string(),
2010            )));
2011        };
2012
2013        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2014            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2015                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2016            )))
2017        })?;
2018
2019        // Make sure to only set this if there are no errors.
2020        self.program = new_program.clone();
2021
2022        // Truncate after the sketch block for mock execution.
2023        let mut truncated_program = new_program;
2024        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2025            .map_err(KclErrorWithOutputs::no_outputs)?;
2026
2027        // Execute.
2028        let outcome = ctx
2029            .run_mock(
2030                &truncated_program,
2031                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2032            )
2033            .await?;
2034
2035        let new_object_ids = {
2036            let make_err =
2037                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2038            let segment_id = outcome
2039                .source_range_to_object
2040                .get(&point_node_ref.range)
2041                .copied()
2042                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2043            let segment_object = outcome
2044                .scene_objects
2045                .get(segment_id.0)
2046                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2047            let ObjectKind::Segment { segment } = &segment_object.kind else {
2048                return Err(make_err(format!(
2049                    "Object is not a segment, it is {}",
2050                    segment_object.kind.human_friendly_kind_with_article()
2051                )));
2052            };
2053            let Segment::Point(_) = segment else {
2054                return Err(make_err(format!(
2055                    "Segment is not a point, it is {}",
2056                    segment.human_friendly_kind_with_article()
2057                )));
2058            };
2059            vec![segment_id]
2060        };
2061        let src_delta = SourceDelta { text: new_source };
2062        // Uses .no_freedom_analysis() so freedom_analysis: false
2063        let outcome = self.update_state_after_exec(outcome, false);
2064        let scene_graph_delta = SceneGraphDelta {
2065            new_graph: self.scene_graph_for_ui(),
2066            invalidates_ids: false,
2067            new_objects: new_object_ids,
2068            exec_outcome: outcome,
2069        };
2070        Ok((src_delta, scene_graph_delta))
2071    }
2072
2073    async fn add_line(
2074        &mut self,
2075        ctx: &ExecutorContext,
2076        sketch: ObjectId,
2077        ctor: LineCtor,
2078    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2079        // Create updated KCL source from args.
2080        let start_ast = to_ast_point2d(&ctor.start)
2081            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082        let end_ast = to_ast_point2d(&ctor.end)
2083            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084        let mut arguments = vec![
2085            ast::LabeledArg {
2086                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2087                arg: start_ast,
2088            },
2089            ast::LabeledArg {
2090                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2091                arg: end_ast,
2092            },
2093        ];
2094        // Add construction kwarg if construction is Some(true)
2095        if ctor.construction == Some(true) {
2096            arguments.push(ast::LabeledArg {
2097                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2098                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2099                    value: ast::LiteralValue::Bool(true),
2100                    raw: "true".to_string(),
2101                    digest: None,
2102                }))),
2103            });
2104        }
2105        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2106            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2107            unlabeled: None,
2108            arguments,
2109            digest: None,
2110            non_code_meta: Default::default(),
2111        })));
2112
2113        // Look up existing sketch.
2114        let sketch_id = sketch;
2115        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2116            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2117        })?;
2118        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2119            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2120                "Object is not a sketch, it is {}",
2121                sketch_object.kind.human_friendly_kind_with_article(),
2122            ))));
2123        };
2124        // Add the line to the AST of the sketch block.
2125        let mut new_ast = self.program.ast.clone();
2126        let (sketch_block_ref, _) = self
2127            .mutate_ast(
2128                &mut new_ast,
2129                sketch_id,
2130                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2131            )
2132            .map_err(KclErrorWithOutputs::no_outputs)?;
2133        // Convert to string source to create real source ranges.
2134        let new_source = source_from_ast(&new_ast);
2135        // Parse the new KCL source.
2136        let (new_program, errors) = Program::parse(&new_source)
2137            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2138        if !errors.is_empty() {
2139            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2140                "Error parsing KCL source after adding line: {errors:?}"
2141            ))));
2142        }
2143        let Some(new_program) = new_program else {
2144            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2145                "No AST produced after adding line".to_string(),
2146            )));
2147        };
2148
2149        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2150            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2151                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2152            )))
2153        })?;
2154
2155        // Make sure to only set this if there are no errors.
2156        self.program = new_program.clone();
2157
2158        // Truncate after the sketch block for mock execution.
2159        let mut truncated_program = new_program;
2160        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2161            .map_err(KclErrorWithOutputs::no_outputs)?;
2162
2163        // Execute.
2164        let outcome = ctx
2165            .run_mock(
2166                &truncated_program,
2167                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2168            )
2169            .await?;
2170
2171        let new_object_ids = {
2172            let make_err =
2173                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2174            let segment_id = outcome
2175                .source_range_to_object
2176                .get(&line_node_ref.range)
2177                .copied()
2178                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2179            let segment_object = outcome
2180                .scene_object_by_id(segment_id)
2181                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2182            let ObjectKind::Segment { segment } = &segment_object.kind else {
2183                return Err(make_err(format!(
2184                    "Object is not a segment, it is {}",
2185                    segment_object.kind.human_friendly_kind_with_article()
2186                )));
2187            };
2188            let Segment::Line(line) = segment else {
2189                return Err(make_err(format!(
2190                    "Segment is not a line, it is {}",
2191                    segment.human_friendly_kind_with_article()
2192                )));
2193            };
2194            vec![line.start, line.end, segment_id]
2195        };
2196        let src_delta = SourceDelta { text: new_source };
2197        // Uses .no_freedom_analysis() so freedom_analysis: false
2198        let outcome = self.update_state_after_exec(outcome, false);
2199        let scene_graph_delta = SceneGraphDelta {
2200            new_graph: self.scene_graph_for_ui(),
2201            invalidates_ids: false,
2202            new_objects: new_object_ids,
2203            exec_outcome: outcome,
2204        };
2205        Ok((src_delta, scene_graph_delta))
2206    }
2207
2208    async fn add_arc(
2209        &mut self,
2210        ctx: &ExecutorContext,
2211        sketch: ObjectId,
2212        ctor: ArcCtor,
2213    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2214        // Create updated KCL source from args.
2215        let start_ast = to_ast_point2d(&ctor.start)
2216            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2217        let end_ast = to_ast_point2d(&ctor.end)
2218            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219        let center_ast = to_ast_point2d(&ctor.center)
2220            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221        let mut arguments = vec![
2222            ast::LabeledArg {
2223                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2224                arg: start_ast,
2225            },
2226            ast::LabeledArg {
2227                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2228                arg: end_ast,
2229            },
2230            ast::LabeledArg {
2231                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2232                arg: center_ast,
2233            },
2234        ];
2235        // Add construction kwarg if construction is Some(true)
2236        if ctor.construction == Some(true) {
2237            arguments.push(ast::LabeledArg {
2238                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2239                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2240                    value: ast::LiteralValue::Bool(true),
2241                    raw: "true".to_string(),
2242                    digest: None,
2243                }))),
2244            });
2245        }
2246        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2247            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2248            unlabeled: None,
2249            arguments,
2250            digest: None,
2251            non_code_meta: Default::default(),
2252        })));
2253
2254        // Look up existing sketch.
2255        let sketch_id = sketch;
2256        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2257            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2258        })?;
2259        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2260            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2261                "Object is not a sketch, it is {}",
2262                sketch_object.kind.human_friendly_kind_with_article(),
2263            ))));
2264        };
2265        // Add the arc to the AST of the sketch block.
2266        let mut new_ast = self.program.ast.clone();
2267        let (sketch_block_ref, _) = self
2268            .mutate_ast(
2269                &mut new_ast,
2270                sketch_id,
2271                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2272            )
2273            .map_err(KclErrorWithOutputs::no_outputs)?;
2274        // Convert to string source to create real source ranges.
2275        let new_source = source_from_ast(&new_ast);
2276        // Parse the new KCL source.
2277        let (new_program, errors) = Program::parse(&new_source)
2278            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2279        if !errors.is_empty() {
2280            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2281                "Error parsing KCL source after adding arc: {errors:?}"
2282            ))));
2283        }
2284        let Some(new_program) = new_program else {
2285            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2286                "No AST produced after adding arc".to_string(),
2287            )));
2288        };
2289
2290        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2291            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2292                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2293            )))
2294        })?;
2295
2296        // Make sure to only set this if there are no errors.
2297        self.program = new_program.clone();
2298
2299        // Truncate after the sketch block for mock execution.
2300        let mut truncated_program = new_program;
2301        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2302            .map_err(KclErrorWithOutputs::no_outputs)?;
2303
2304        // Execute.
2305        let outcome = ctx
2306            .run_mock(
2307                &truncated_program,
2308                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2309            )
2310            .await?;
2311
2312        let new_object_ids = {
2313            let make_err =
2314                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2315            let segment_id = outcome
2316                .source_range_to_object
2317                .get(&arc_node_ref.range)
2318                .copied()
2319                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2320            let segment_object = outcome
2321                .scene_objects
2322                .get(segment_id.0)
2323                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2324            let ObjectKind::Segment { segment } = &segment_object.kind else {
2325                return Err(make_err(format!(
2326                    "Object is not a segment, it is {}",
2327                    segment_object.kind.human_friendly_kind_with_article()
2328                )));
2329            };
2330            let Segment::Arc(arc) = segment else {
2331                return Err(make_err(format!(
2332                    "Segment is not an arc, it is {}",
2333                    segment.human_friendly_kind_with_article()
2334                )));
2335            };
2336            vec![arc.start, arc.end, arc.center, segment_id]
2337        };
2338        let src_delta = SourceDelta { text: new_source };
2339        // Uses .no_freedom_analysis() so freedom_analysis: false
2340        let outcome = self.update_state_after_exec(outcome, false);
2341        let scene_graph_delta = SceneGraphDelta {
2342            new_graph: self.scene_graph_for_ui(),
2343            invalidates_ids: false,
2344            new_objects: new_object_ids,
2345            exec_outcome: outcome,
2346        };
2347        Ok((src_delta, scene_graph_delta))
2348    }
2349
2350    async fn add_circle(
2351        &mut self,
2352        ctx: &ExecutorContext,
2353        sketch: ObjectId,
2354        ctor: CircleCtor,
2355    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2356        // Create updated KCL source from args.
2357        let start_ast = to_ast_point2d(&ctor.start)
2358            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2359        let center_ast = to_ast_point2d(&ctor.center)
2360            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2361        let mut arguments = vec![
2362            ast::LabeledArg {
2363                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2364                arg: start_ast,
2365            },
2366            ast::LabeledArg {
2367                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2368                arg: center_ast,
2369            },
2370        ];
2371        // Add construction kwarg if construction is Some(true)
2372        if ctor.construction == Some(true) {
2373            arguments.push(ast::LabeledArg {
2374                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2375                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2376                    value: ast::LiteralValue::Bool(true),
2377                    raw: "true".to_string(),
2378                    digest: None,
2379                }))),
2380            });
2381        }
2382        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2383            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2384            unlabeled: None,
2385            arguments,
2386            digest: None,
2387            non_code_meta: Default::default(),
2388        })));
2389
2390        // Look up existing sketch.
2391        let sketch_id = sketch;
2392        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2393            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2394        })?;
2395        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2396            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2397                "Object is not a sketch, it is {}",
2398                sketch_object.kind.human_friendly_kind_with_article(),
2399            ))));
2400        };
2401        // Add the circle to the AST of the sketch block.
2402        let mut new_ast = self.program.ast.clone();
2403        let (sketch_block_ref, _) = self
2404            .mutate_ast(
2405                &mut new_ast,
2406                sketch_id,
2407                AstMutateCommand::AddSketchBlockVarDecl {
2408                    prefix: CIRCLE_VARIABLE.to_owned(),
2409                    expr: circle_ast,
2410                },
2411            )
2412            .map_err(KclErrorWithOutputs::no_outputs)?;
2413        // Convert to string source to create real source ranges.
2414        let new_source = source_from_ast(&new_ast);
2415        // Parse the new KCL source.
2416        let (new_program, errors) = Program::parse(&new_source)
2417            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2418        if !errors.is_empty() {
2419            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2420                "Error parsing KCL source after adding circle: {errors:?}"
2421            ))));
2422        }
2423        let Some(new_program) = new_program else {
2424            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2425                "No AST produced after adding circle".to_string(),
2426            )));
2427        };
2428
2429        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2430            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2431                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2432            )))
2433        })?;
2434
2435        // Make sure to only set this if there are no errors.
2436        self.program = new_program.clone();
2437
2438        // Truncate after the sketch block for mock execution.
2439        let mut truncated_program = new_program;
2440        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2441            .map_err(KclErrorWithOutputs::no_outputs)?;
2442
2443        // Execute.
2444        let outcome = ctx
2445            .run_mock(
2446                &truncated_program,
2447                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2448            )
2449            .await?;
2450
2451        let new_object_ids = {
2452            let make_err =
2453                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2454            let segment_id = outcome
2455                .source_range_to_object
2456                .get(&circle_node_ref.range)
2457                .copied()
2458                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2459            let segment_object = outcome
2460                .scene_objects
2461                .get(segment_id.0)
2462                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2463            let ObjectKind::Segment { segment } = &segment_object.kind else {
2464                return Err(make_err(format!(
2465                    "Object is not a segment, it is {}",
2466                    segment_object.kind.human_friendly_kind_with_article()
2467                )));
2468            };
2469            let Segment::Circle(circle) = segment else {
2470                return Err(make_err(format!(
2471                    "Segment is not a circle, it is {}",
2472                    segment.human_friendly_kind_with_article()
2473                )));
2474            };
2475            vec![circle.start, circle.center, segment_id]
2476        };
2477        let src_delta = SourceDelta { text: new_source };
2478        // Uses .no_freedom_analysis() so freedom_analysis: false
2479        let outcome = self.update_state_after_exec(outcome, false);
2480        let scene_graph_delta = SceneGraphDelta {
2481            new_graph: self.scene_graph_for_ui(),
2482            invalidates_ids: false,
2483            new_objects: new_object_ids,
2484            exec_outcome: outcome,
2485        };
2486        Ok((src_delta, scene_graph_delta))
2487    }
2488
2489    async fn add_control_point_spline(
2490        &mut self,
2491        ctx: &ExecutorContext,
2492        sketch: ObjectId,
2493        ctor: ControlPointSplineCtor,
2494    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2495        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2496            .map_err(KclErrorWithOutputs::no_outputs)?;
2497
2498        let points_ast = to_ast_point2d_array(&ctor.points)
2499            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2500        let mut arguments = vec![ast::LabeledArg {
2501            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2502            arg: points_ast,
2503        }];
2504        if ctor.construction == Some(true) {
2505            arguments.push(ast::LabeledArg {
2506                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2507                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2508                    value: ast::LiteralValue::Bool(true),
2509                    raw: "true".to_string(),
2510                    digest: None,
2511                }))),
2512            });
2513        }
2514        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2515            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2516            unlabeled: None,
2517            arguments,
2518            digest: None,
2519            non_code_meta: Default::default(),
2520        })));
2521
2522        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2523            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2524        })?;
2525        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2526            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2527                "Object is not a sketch, it is {}",
2528                sketch_object.kind.human_friendly_kind_with_article(),
2529            ))));
2530        };
2531
2532        let mut new_ast = new_program.ast.clone();
2533        let (sketch_block_ref, _) = self
2534            .mutate_ast(
2535                &mut new_ast,
2536                sketch,
2537                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2538            )
2539            .map_err(KclErrorWithOutputs::no_outputs)?;
2540        let new_source = source_from_ast(&new_ast);
2541        let (new_program, errors) = Program::parse(&new_source)
2542            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2543        if !errors.is_empty() {
2544            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2545                "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2546            ))));
2547        }
2548        let Some(new_program) = new_program else {
2549            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2550                "No AST produced after adding controlPointSpline".to_string(),
2551            )));
2552        };
2553
2554        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2555            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2556                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2557            )))
2558        })?;
2559        #[cfg(not(feature = "artifact-graph"))]
2560        let _ = spline_node_ref;
2561
2562        self.program = new_program.clone();
2563
2564        let mut truncated_program = new_program;
2565        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2566            .map_err(KclErrorWithOutputs::no_outputs)?;
2567
2568        let outcome = ctx
2569            .run_mock(
2570                &truncated_program,
2571                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2572            )
2573            .await?;
2574
2575        #[cfg(not(feature = "artifact-graph"))]
2576        let new_object_ids = Vec::new();
2577        #[cfg(feature = "artifact-graph")]
2578        let new_object_ids = {
2579            let make_err =
2580                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2581            let segment_id = outcome
2582                .source_range_to_object
2583                .get(&spline_node_ref.range)
2584                .copied()
2585                .ok_or_else(|| {
2586                    make_err(format!(
2587                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2588                    ))
2589                })?;
2590            let segment_object = outcome
2591                .scene_objects
2592                .get(segment_id.0)
2593                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2594            let ObjectKind::Segment { segment } = &segment_object.kind else {
2595                return Err(make_err(format!(
2596                    "Object is not a segment, it is {}",
2597                    segment_object.kind.human_friendly_kind_with_article()
2598                )));
2599            };
2600            let Segment::ControlPointSpline(spline) = segment else {
2601                return Err(make_err(format!(
2602                    "Segment is not a control point spline, it is {}",
2603                    segment.human_friendly_kind_with_article()
2604                )));
2605            };
2606
2607            let mut ids = outcome
2608                .scene_objects
2609                .iter()
2610                .filter_map(|obj| match &obj.kind {
2611                    ObjectKind::Segment {
2612                        segment: Segment::Line(line),
2613                    } if line.owner == Some(segment_id) => Some(obj.id),
2614                    _ => None,
2615                })
2616                .collect::<Vec<_>>();
2617            ids.extend(spline.controls.clone());
2618            ids.push(segment_id);
2619            ids
2620        };
2621        let src_delta = SourceDelta { text: new_source };
2622        let outcome = self.update_state_after_exec(outcome, false);
2623        let scene_graph_delta = SceneGraphDelta {
2624            new_graph: self.scene_graph_for_ui(),
2625            invalidates_ids: false,
2626            new_objects: new_object_ids,
2627            exec_outcome: outcome,
2628        };
2629        Ok((src_delta, scene_graph_delta))
2630    }
2631
2632    fn edit_point(
2633        &mut self,
2634        new_ast: &mut ast::Node<ast::Program>,
2635        sketch: ObjectId,
2636        point: ObjectId,
2637        ctor: PointCtor,
2638    ) -> Result<(), KclError> {
2639        // Create updated KCL source from args.
2640        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2641
2642        // Look up existing sketch.
2643        let sketch_id = sketch;
2644        let sketch_object = self
2645            .scene_graph
2646            .objects
2647            .get(sketch_id.0)
2648            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2649        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2650            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2651        };
2652        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2653            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2654        })?;
2655        // Look up existing point.
2656        let point_id = point;
2657        let point_object = self
2658            .scene_graph
2659            .objects
2660            .get(point_id.0)
2661            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2662        let ObjectKind::Segment {
2663            segment: Segment::Point(point),
2664        } = &point_object.kind
2665        else {
2666            return Err(KclError::refactor(format!(
2667                "Object is not a point segment: {point_object:?}"
2668            )));
2669        };
2670
2671        // If the point is part of a line or arc, edit the line/arc instead.
2672        if let Some(owner_id) = point.owner {
2673            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2674                KclError::refactor(format!(
2675                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2676                ))
2677            })?;
2678            let ObjectKind::Segment { segment } = &owner_object.kind else {
2679                return Err(KclError::refactor(format!(
2680                    "Internal: Owner of point is not a segment, but found {}",
2681                    owner_object.kind.human_friendly_kind_with_article()
2682                )));
2683            };
2684
2685            // Handle Line owner
2686            if let Segment::Line(line) = segment {
2687                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2688                    return Err(KclError::refactor(format!(
2689                        "Internal: Owner of point does not have line ctor, but found {}",
2690                        line.ctor.human_friendly_kind_with_article()
2691                    )));
2692                };
2693                let mut line_ctor = line_ctor.clone();
2694                // Which end of the line is this point?
2695                if line.start == point_id {
2696                    line_ctor.start = ctor.position;
2697                } else if line.end == point_id {
2698                    line_ctor.end = ctor.position;
2699                } else {
2700                    return Err(KclError::refactor(format!(
2701                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2702                    )));
2703                }
2704                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2705            }
2706
2707            // Handle Arc owner
2708            if let Segment::Arc(arc) = segment {
2709                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2710                    return Err(KclError::refactor(format!(
2711                        "Internal: Owner of point does not have arc ctor, but found {}",
2712                        arc.ctor.human_friendly_kind_with_article()
2713                    )));
2714                };
2715                let mut arc_ctor = arc_ctor.clone();
2716                // Which point of the arc is this? (center, start, or end)
2717                if arc.center == point_id {
2718                    arc_ctor.center = ctor.position;
2719                } else if arc.start == point_id {
2720                    arc_ctor.start = ctor.position;
2721                } else if arc.end == point_id {
2722                    arc_ctor.end = ctor.position;
2723                } else {
2724                    return Err(KclError::refactor(format!(
2725                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2726                    )));
2727                }
2728                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2729            }
2730
2731            // Handle Circle owner
2732            if let Segment::Circle(circle) = segment {
2733                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2734                    return Err(KclError::refactor(format!(
2735                        "Internal: Owner of point does not have circle ctor, but found {}",
2736                        circle.ctor.human_friendly_kind_with_article()
2737                    )));
2738                };
2739                let mut circle_ctor = circle_ctor.clone();
2740                if circle.center == point_id {
2741                    circle_ctor.center = ctor.position;
2742                } else if circle.start == point_id {
2743                    circle_ctor.start = ctor.position;
2744                } else {
2745                    return Err(KclError::refactor(format!(
2746                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2747                    )));
2748                }
2749                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2750            }
2751
2752            if let Segment::ControlPointSpline(spline) = segment {
2753                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2754                    return Err(KclError::refactor(format!(
2755                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2756                        spline.ctor.human_friendly_kind_with_article()
2757                    )));
2758                };
2759                let mut spline_ctor = spline_ctor.clone();
2760                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2761                    return Err(KclError::refactor(format!(
2762                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2763                    )));
2764                };
2765                spline_ctor.points[control_index] = ctor.position;
2766                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2767            }
2768
2769            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2770            // (fall through to the point editing logic below)
2771        }
2772
2773        // Modify the point AST.
2774        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2775        Ok(())
2776    }
2777
2778    fn edit_line(
2779        &mut self,
2780        new_ast: &mut ast::Node<ast::Program>,
2781        sketch: ObjectId,
2782        line: ObjectId,
2783        ctor: LineCtor,
2784    ) -> Result<(), KclError> {
2785        // Create updated KCL source from args.
2786        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2787        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2788
2789        // Look up existing sketch.
2790        let sketch_id = sketch;
2791        let sketch_object = self
2792            .scene_graph
2793            .objects
2794            .get(sketch_id.0)
2795            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2796        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2797            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2798        };
2799        sketch
2800            .segments
2801            .iter()
2802            .find(|o| **o == line)
2803            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2804        // Look up existing line.
2805        let line_id = line;
2806        let line_object = self
2807            .scene_graph
2808            .objects
2809            .get(line_id.0)
2810            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2811        let ObjectKind::Segment { .. } = &line_object.kind else {
2812            let kind = line_object.kind.human_friendly_kind_with_article();
2813            return Err(KclError::refactor(format!(
2814                "This constraint only works on Segments, but you selected {kind}"
2815            )));
2816        };
2817
2818        // Modify the line AST.
2819        self.mutate_ast(
2820            new_ast,
2821            line_id,
2822            AstMutateCommand::EditLine {
2823                start: new_start_ast,
2824                end: new_end_ast,
2825                construction: ctor.construction,
2826            },
2827        )?;
2828        Ok(())
2829    }
2830
2831    fn edit_arc(
2832        &mut self,
2833        new_ast: &mut ast::Node<ast::Program>,
2834        sketch: ObjectId,
2835        arc: ObjectId,
2836        ctor: ArcCtor,
2837    ) -> Result<(), KclError> {
2838        // Create updated KCL source from args.
2839        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2840        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2841        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2842
2843        // Look up existing sketch.
2844        let sketch_id = sketch;
2845        let sketch_object = self
2846            .scene_graph
2847            .objects
2848            .get(sketch_id.0)
2849            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2850        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2851            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2852        };
2853        sketch
2854            .segments
2855            .iter()
2856            .find(|o| **o == arc)
2857            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2858        // Look up existing arc.
2859        let arc_id = arc;
2860        let arc_object = self
2861            .scene_graph
2862            .objects
2863            .get(arc_id.0)
2864            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2865        let ObjectKind::Segment { .. } = &arc_object.kind else {
2866            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2867        };
2868
2869        // Modify the arc AST.
2870        self.mutate_ast(
2871            new_ast,
2872            arc_id,
2873            AstMutateCommand::EditArc {
2874                start: new_start_ast,
2875                end: new_end_ast,
2876                center: new_center_ast,
2877                construction: ctor.construction,
2878            },
2879        )?;
2880        Ok(())
2881    }
2882
2883    fn edit_circle(
2884        &mut self,
2885        new_ast: &mut ast::Node<ast::Program>,
2886        sketch: ObjectId,
2887        circle: ObjectId,
2888        ctor: CircleCtor,
2889    ) -> Result<(), KclError> {
2890        // Create updated KCL source from args.
2891        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2892        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2893
2894        // Look up existing sketch.
2895        let sketch_id = sketch;
2896        let sketch_object = self
2897            .scene_graph
2898            .objects
2899            .get(sketch_id.0)
2900            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2901        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2902            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2903        };
2904        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2905            KclError::refactor(format!(
2906                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2907            ))
2908        })?;
2909        // Look up existing circle.
2910        let circle_id = circle;
2911        let circle_object = self
2912            .scene_graph
2913            .objects
2914            .get(circle_id.0)
2915            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2916        let ObjectKind::Segment { .. } = &circle_object.kind else {
2917            return Err(KclError::refactor(format!(
2918                "Object is not a segment: {circle_object:?}"
2919            )));
2920        };
2921
2922        // Modify the circle AST.
2923        self.mutate_ast(
2924            new_ast,
2925            circle_id,
2926            AstMutateCommand::EditCircle {
2927                start: new_start_ast,
2928                center: new_center_ast,
2929                construction: ctor.construction,
2930            },
2931        )?;
2932        Ok(())
2933    }
2934
2935    fn edit_control_point_spline(
2936        &mut self,
2937        new_ast: &mut ast::Node<ast::Program>,
2938        sketch: ObjectId,
2939        spline: ObjectId,
2940        ctor: ControlPointSplineCtor,
2941    ) -> Result<(), KclError> {
2942        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2943
2944        let sketch_object = self
2945            .scene_graph
2946            .objects
2947            .get(sketch.0)
2948            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951        };
2952        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2953            KclError::refactor(format!(
2954                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2955            ))
2956        })?;
2957
2958        let spline_object =
2959            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2960                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2961            })?;
2962        let ObjectKind::Segment { .. } = &spline_object.kind else {
2963            return Err(KclError::refactor(format!(
2964                "Object is not a segment: {spline_object:?}"
2965            )));
2966        };
2967
2968        self.mutate_ast(
2969            new_ast,
2970            spline,
2971            AstMutateCommand::EditControlPointSpline {
2972                points: points_ast,
2973                construction: ctor.construction,
2974            },
2975        )?;
2976        Ok(())
2977    }
2978
2979    fn delete_segment(
2980        &mut self,
2981        new_ast: &mut ast::Node<ast::Program>,
2982        sketch: ObjectId,
2983        segment_id: ObjectId,
2984    ) -> Result<(), KclError> {
2985        // Look up existing sketch.
2986        let sketch_id = sketch;
2987        let sketch_object = self
2988            .scene_graph
2989            .objects
2990            .get(sketch_id.0)
2991            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2992        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2993            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2994        };
2995        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2996            KclError::refactor(format!(
2997                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2998            ))
2999        })?;
3000        // Look up existing segment.
3001        let segment_object =
3002            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3003                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3004            })?;
3005        let ObjectKind::Segment { .. } = &segment_object.kind else {
3006            return Err(KclError::refactor(format!(
3007                "Object is not a segment, it is {}",
3008                segment_object.kind.human_friendly_kind_with_article()
3009            )));
3010        };
3011
3012        // Modify the AST to remove the segment.
3013        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3014        Ok(())
3015    }
3016
3017    fn delete_constraint(
3018        &mut self,
3019        new_ast: &mut ast::Node<ast::Program>,
3020        sketch: ObjectId,
3021        constraint_id: ObjectId,
3022    ) -> Result<(), KclError> {
3023        // Look up existing sketch.
3024        let sketch_id = sketch;
3025        let sketch_object = self
3026            .scene_graph
3027            .objects
3028            .get(sketch_id.0)
3029            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3030        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3031            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3032        };
3033        sketch
3034            .constraints
3035            .iter()
3036            .find(|o| **o == constraint_id)
3037            .ok_or_else(|| {
3038                KclError::refactor(format!(
3039                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3040                ))
3041            })?;
3042        // Look up existing constraint.
3043        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3044            KclError::refactor(format!(
3045                "Constraint not found in scene graph: constraint={constraint_id:?}"
3046            ))
3047        })?;
3048        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3049            return Err(KclError::refactor(format!(
3050                "Object is not a constraint, it is {}",
3051                constraint_object.kind.human_friendly_kind_with_article()
3052            )));
3053        };
3054
3055        // Modify the AST to remove the constraint.
3056        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3057        Ok(())
3058    }
3059
3060    fn edit_coincident_constraint(
3061        &mut self,
3062        new_ast: &mut ast::Node<ast::Program>,
3063        constraint_id: ObjectId,
3064        segments: Vec<ConstraintSegment>,
3065    ) -> Result<(), KclError> {
3066        if segments.len() < 2 {
3067            return Err(KclError::refactor(format!(
3068                "Coincident constraint must have at least 2 inputs, got {}",
3069                segments.len()
3070            )));
3071        }
3072
3073        let segment_asts = segments
3074            .iter()
3075            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3076            .collect::<Result<Vec<_>, _>>()?;
3077
3078        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3079            elements: segment_asts,
3080            digest: None,
3081            non_code_meta: Default::default(),
3082        })));
3083
3084        self.mutate_ast(
3085            new_ast,
3086            constraint_id,
3087            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3088        )?;
3089        Ok(())
3090    }
3091
3092    fn edit_horizontal_points_constraint(
3093        &mut self,
3094        new_ast: &mut ast::Node<ast::Program>,
3095        constraint_id: ObjectId,
3096        points: Vec<ConstraintSegment>,
3097    ) -> Result<(), KclError> {
3098        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3099    }
3100
3101    fn edit_vertical_points_constraint(
3102        &mut self,
3103        new_ast: &mut ast::Node<ast::Program>,
3104        constraint_id: ObjectId,
3105        points: Vec<ConstraintSegment>,
3106    ) -> Result<(), KclError> {
3107        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3108    }
3109
3110    fn edit_axis_points_constraint(
3111        &mut self,
3112        new_ast: &mut ast::Node<ast::Program>,
3113        constraint_id: ObjectId,
3114        points: Vec<ConstraintSegment>,
3115        constraint_name: &str,
3116    ) -> Result<(), KclError> {
3117        if points.len() < 2 {
3118            return Err(KclError::refactor(format!(
3119                "{constraint_name} points constraint must have at least 2 points, got {}",
3120                points.len()
3121            )));
3122        }
3123
3124        let point_asts = points
3125            .iter()
3126            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3127            .collect::<Result<Vec<_>, _>>()?;
3128
3129        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3130            elements: point_asts,
3131            digest: None,
3132            non_code_meta: Default::default(),
3133        })));
3134
3135        self.mutate_ast(
3136            new_ast,
3137            constraint_id,
3138            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3139        )?;
3140        Ok(())
3141    }
3142
3143    /// updates the equalLength constraint with the given lines
3144    fn edit_equal_length_constraint(
3145        &mut self,
3146        new_ast: &mut ast::Node<ast::Program>,
3147        constraint_id: ObjectId,
3148        lines: Vec<ObjectId>,
3149    ) -> Result<(), KclError> {
3150        if lines.len() < 2 {
3151            return Err(KclError::refactor(format!(
3152                "Lines equal length constraint must have at least 2 lines, got {}",
3153                lines.len()
3154            )));
3155        }
3156
3157        let line_asts = lines
3158            .iter()
3159            .map(|line_id| {
3160                let line_object = self
3161                    .scene_graph
3162                    .objects
3163                    .get(line_id.0)
3164                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3165                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3166                    let kind = line_object.kind.human_friendly_kind_with_article();
3167                    return Err(KclError::refactor(format!(
3168                        "This constraint only works on Segments, but you selected {kind}"
3169                    )));
3170                };
3171                let Segment::Line(_) = line_segment else {
3172                    let kind = line_segment.human_friendly_kind_with_article();
3173                    return Err(KclError::refactor(format!(
3174                        "Only lines can be made equal length, but you selected {kind}"
3175                    )));
3176                };
3177
3178                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3179            })
3180            .collect::<Result<Vec<_>, _>>()?;
3181
3182        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3183            elements: line_asts,
3184            digest: None,
3185            non_code_meta: Default::default(),
3186        })));
3187
3188        self.mutate_ast(
3189            new_ast,
3190            constraint_id,
3191            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3192        )?;
3193        Ok(())
3194    }
3195
3196    /// Updates the parallel constraint with the given lines.
3197    fn edit_parallel_constraint(
3198        &mut self,
3199        new_ast: &mut ast::Node<ast::Program>,
3200        constraint_id: ObjectId,
3201        lines: Vec<ObjectId>,
3202    ) -> Result<(), KclError> {
3203        if lines.len() < 2 {
3204            return Err(KclError::refactor(format!(
3205                "Parallel constraint must have at least 2 lines, got {}",
3206                lines.len()
3207            )));
3208        }
3209
3210        let line_asts = lines
3211            .iter()
3212            .map(|line_id| {
3213                let line_object = self
3214                    .scene_graph
3215                    .objects
3216                    .get(line_id.0)
3217                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3218                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3219                    let kind = line_object.kind.human_friendly_kind_with_article();
3220                    return Err(KclError::refactor(format!(
3221                        "This constraint only works on Segments, but you selected {kind}"
3222                    )));
3223                };
3224                let Segment::Line(_) = line_segment else {
3225                    let kind = line_segment.human_friendly_kind_with_article();
3226                    return Err(KclError::refactor(format!(
3227                        "Only lines can be made parallel, but you selected {kind}"
3228                    )));
3229                };
3230
3231                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3232            })
3233            .collect::<Result<Vec<_>, _>>()?;
3234
3235        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3236            elements: line_asts,
3237            digest: None,
3238            non_code_meta: Default::default(),
3239        })));
3240
3241        self.mutate_ast(
3242            new_ast,
3243            constraint_id,
3244            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3245        )?;
3246        Ok(())
3247    }
3248
3249    /// Updates the equalRadius constraint with the given segments.
3250    fn edit_equal_radius_constraint(
3251        &mut self,
3252        new_ast: &mut ast::Node<ast::Program>,
3253        constraint_id: ObjectId,
3254        input: Vec<ObjectId>,
3255    ) -> Result<(), KclError> {
3256        if input.len() < 2 {
3257            return Err(KclError::refactor(format!(
3258                "equalRadius constraint must have at least 2 segments, got {}",
3259                input.len()
3260            )));
3261        }
3262
3263        let input_asts = input
3264            .iter()
3265            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3266            .collect::<Result<Vec<_>, _>>()?;
3267
3268        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3269            elements: input_asts,
3270            digest: None,
3271            non_code_meta: Default::default(),
3272        })));
3273
3274        self.mutate_ast(
3275            new_ast,
3276            constraint_id,
3277            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3278        )?;
3279        Ok(())
3280    }
3281
3282    async fn execute_after_edit(
3283        &mut self,
3284        ctx: &ExecutorContext,
3285        sketch: ObjectId,
3286        sketch_block_ref: AstNodeRef,
3287        new_ast: &mut ast::Node<ast::Program>,
3288        options: ExecuteAfterEditOptions,
3289    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3290        let ExecuteAfterEditOptions {
3291            segment_ids_edited,
3292            edit_kind,
3293            commit_solved_initial_guesses,
3294        } = options;
3295
3296        // Convert to string source to create real source ranges.
3297        let new_source = source_from_ast(new_ast);
3298        // Parse the new KCL source.
3299        let (new_program, errors) = Program::parse(&new_source)
3300            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3301        if !errors.is_empty() {
3302            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3303                "Error parsing KCL source after editing: {errors:?}"
3304            ))));
3305        }
3306        let Some(new_program) = new_program else {
3307            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3308                "No AST produced after editing".to_string(),
3309            )));
3310        };
3311
3312        // TODO: sketch-api: make sure to only set this if there are no errors.
3313        self.program = new_program.clone();
3314
3315        // Truncate after the sketch block for mock execution.
3316        let is_delete = edit_kind.is_delete();
3317        let truncated_program = {
3318            let mut truncated_program = new_program;
3319            only_sketch_block(
3320                &mut truncated_program.ast,
3321                &sketch_block_ref,
3322                edit_kind.to_change_kind(),
3323            )
3324            .map_err(KclErrorWithOutputs::no_outputs)?;
3325            truncated_program
3326        };
3327
3328        // Execute.
3329        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3330        let mock_config = MockConfig {
3331            sketch_block_id: Some(sketch),
3332            freedom_analysis: is_delete,
3333            segment_ids_edited: segment_ids_edited.clone(),
3334            drag_anchors,
3335            ..Default::default()
3336        };
3337        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3338
3339        // Uses freedom_analysis: is_delete
3340        let outcome = self.update_state_after_exec(outcome, is_delete);
3341
3342        let src_delta = if commit_solved_initial_guesses {
3343            self.commit_var_solutions_to_program(&outcome, "editing")?
3344        } else {
3345            SourceDelta { text: new_source }
3346        };
3347        let scene_graph_delta = SceneGraphDelta {
3348            new_graph: self.scene_graph_for_ui(),
3349            invalidates_ids: is_delete,
3350            new_objects: Vec::new(),
3351            exec_outcome: outcome,
3352        };
3353        Ok((src_delta, scene_graph_delta))
3354    }
3355
3356    async fn execute_after_delete_sketch(
3357        &mut self,
3358        ctx: &ExecutorContext,
3359        new_ast: &mut ast::Node<ast::Program>,
3360    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3361        // Convert to string source to create real source ranges.
3362        let new_source = source_from_ast(new_ast);
3363        // Parse the new KCL source.
3364        let (new_program, errors) = Program::parse(&new_source)
3365            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3366        if !errors.is_empty() {
3367            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3368                "Error parsing KCL source after editing: {errors:?}"
3369            ))));
3370        }
3371        let Some(new_program) = new_program else {
3372            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3373                "No AST produced after editing".to_string(),
3374            )));
3375        };
3376
3377        // Make sure to only set this if there are no errors.
3378        self.program = new_program.clone();
3379
3380        // We deleted the entire sketch block. It doesn't make sense to truncate
3381        // and execute only the sketch block. We execute the whole program with
3382        // a real engine.
3383
3384        // Execute.
3385        let outcome = ctx.run_with_caching(new_program).await?;
3386        let freedom_analysis_ran = true;
3387
3388        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3389
3390        let src_delta = SourceDelta { text: new_source };
3391        let scene_graph_delta = SceneGraphDelta {
3392            new_graph: self.scene_graph_for_ui(),
3393            invalidates_ids: true,
3394            new_objects: Vec::new(),
3395            exec_outcome: outcome,
3396        };
3397        Ok((src_delta, scene_graph_delta))
3398    }
3399
3400    /// Map a point object id into an AST reference expression for use in
3401    /// constraints. If the point is owned by a segment (line or arc), we
3402    /// reference the appropriate property on that segment (e.g. `line1.start`,
3403    /// `arc1.center`). Otherwise we reference the point directly.
3404    fn point_id_to_ast_reference(
3405        &self,
3406        point_id: ObjectId,
3407        new_ast: &mut ast::Node<ast::Program>,
3408    ) -> Result<ast::Expr, KclError> {
3409        let point_object = self
3410            .scene_graph
3411            .objects
3412            .get(point_id.0)
3413            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3414        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3415            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3416        };
3417        let Segment::Point(point) = point_segment else {
3418            return Err(KclError::refactor(format!(
3419                "Only points are currently supported: {point_object:?}"
3420            )));
3421        };
3422
3423        if let Some(owner_id) = point.owner {
3424            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3425                KclError::refactor(format!(
3426                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3427                ))
3428            })?;
3429            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3430                return Err(KclError::refactor(format!(
3431                    "Owner of point is not a segment, but found {}",
3432                    owner_object.kind.human_friendly_kind_with_article()
3433                )));
3434            };
3435
3436            match owner_segment {
3437                Segment::Line(line) => {
3438                    let property = if line.start == point_id {
3439                        LINE_PROPERTY_START
3440                    } else if line.end == point_id {
3441                        LINE_PROPERTY_END
3442                    } else {
3443                        return Err(KclError::refactor(format!(
3444                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3445                        )));
3446                    };
3447                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3448                }
3449                Segment::Arc(arc) => {
3450                    let property = if arc.start == point_id {
3451                        ARC_PROPERTY_START
3452                    } else if arc.end == point_id {
3453                        ARC_PROPERTY_END
3454                    } else if arc.center == point_id {
3455                        ARC_PROPERTY_CENTER
3456                    } else {
3457                        return Err(KclError::refactor(format!(
3458                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3459                        )));
3460                    };
3461                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3462                }
3463                Segment::Circle(circle) => {
3464                    let property = if circle.start == point_id {
3465                        CIRCLE_PROPERTY_START
3466                    } else if circle.center == point_id {
3467                        CIRCLE_PROPERTY_CENTER
3468                    } else {
3469                        return Err(KclError::refactor(format!(
3470                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3471                        )));
3472                    };
3473                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3474                }
3475                Segment::ControlPointSpline(spline) => {
3476                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3477                        return Err(KclError::refactor(format!(
3478                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3479                        )));
3480                    };
3481                    let owner_expr =
3482                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3483                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3484                    Ok(create_index_expression(controls_expr, index))
3485                }
3486                _ => Err(KclError::refactor(format!(
3487                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3488                ))),
3489            }
3490        } else {
3491            // Standalone point.
3492            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3493        }
3494    }
3495
3496    fn line_id_to_ast_reference(
3497        &self,
3498        line_id: ObjectId,
3499        new_ast: &mut ast::Node<ast::Program>,
3500    ) -> Result<ast::Expr, KclError> {
3501        let line_object = self
3502            .scene_graph
3503            .objects
3504            .get(line_id.0)
3505            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3506        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3507            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3508        };
3509        let Segment::Line(line) = line_segment else {
3510            return Err(KclError::refactor(format!(
3511                "Only lines are currently supported: {line_object:?}"
3512            )));
3513        };
3514
3515        if let Some(owner_id) = line.owner {
3516            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3517                KclError::refactor(format!(
3518                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3519                ))
3520            })?;
3521            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3522                return Err(KclError::refactor(format!(
3523                    "Owner of line is not a segment, but found {}",
3524                    owner_object.kind.human_friendly_kind_with_article()
3525                )));
3526            };
3527
3528            match owner_segment {
3529                Segment::ControlPointSpline(spline) => {
3530                    let Some(index) = spline
3531                        .controls
3532                        .windows(2)
3533                        .position(|window| window[0] == line.start && window[1] == line.end)
3534                    else {
3535                        return Err(KclError::refactor(format!(
3536                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3537                        )));
3538                    };
3539                    let owner_expr =
3540                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3541                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3542                    Ok(create_index_expression(edges_expr, index))
3543                }
3544                _ => Err(KclError::refactor(format!(
3545                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3546                ))),
3547            }
3548        } else {
3549            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3550        }
3551    }
3552
3553    fn coincident_segment_to_ast(
3554        &self,
3555        segment: &ConstraintSegment,
3556        new_ast: &mut ast::Node<ast::Program>,
3557    ) -> Result<ast::Expr, KclError> {
3558        match segment {
3559            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3560            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3561        }
3562    }
3563
3564    fn segment_id_to_constraint_ast_reference(
3565        &self,
3566        segment_id: ObjectId,
3567        new_ast: &mut ast::Node<ast::Program>,
3568    ) -> Result<ast::Expr, KclError> {
3569        let segment_object = self
3570            .scene_graph
3571            .objects
3572            .get(segment_id.0)
3573            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3574        let ObjectKind::Segment { segment } = &segment_object.kind else {
3575            return Err(KclError::refactor(format!(
3576                "Object is not a segment, it is {}",
3577                segment_object.kind.human_friendly_kind_with_article()
3578            )));
3579        };
3580
3581        match segment {
3582            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3583            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3584            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3585            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3586            Segment::ControlPointSpline(_) => {
3587                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3588            }
3589        }
3590    }
3591
3592    fn axis_constraint_segment_to_ast(
3593        &self,
3594        segment: &ConstraintSegment,
3595        new_ast: &mut ast::Node<ast::Program>,
3596    ) -> Result<ast::Expr, KclError> {
3597        match segment {
3598            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3599            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3600        }
3601    }
3602
3603    async fn add_coincident(
3604        &mut self,
3605        sketch: ObjectId,
3606        coincident: Coincident,
3607        new_ast: &mut ast::Node<ast::Program>,
3608    ) -> Result<AstNodeRef, KclError> {
3609        let sketch_id = sketch;
3610        for segment in &coincident.segments {
3611            let ConstraintSegment::Segment(segment_id) = segment else {
3612                continue;
3613            };
3614            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3615                continue;
3616            };
3617            if matches!(
3618                segment_object.kind,
3619                ObjectKind::Segment {
3620                    segment: Segment::ControlPointSpline(_)
3621                }
3622            ) {
3623                return Err(KclError::refactor(
3624                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3625                        .to_owned(),
3626                ));
3627            }
3628        }
3629        let segment_asts = coincident
3630            .segments
3631            .iter()
3632            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3633            .collect::<Result<Vec<_>, _>>()?;
3634        if segment_asts.len() < 2 {
3635            return Err(KclError::refactor(format!(
3636                "Coincident constraint must have at least 2 inputs, got {}",
3637                segment_asts.len()
3638            )));
3639        }
3640
3641        // Create the coincident() call using shared helper.
3642        let coincident_ast = create_coincident_ast(segment_asts);
3643
3644        // Add the line to the AST of the sketch block.
3645        let (sketch_block_ref, _) = self.mutate_ast(
3646            new_ast,
3647            sketch_id,
3648            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3649        )?;
3650        Ok(sketch_block_ref)
3651    }
3652
3653    async fn add_distance(
3654        &mut self,
3655        sketch: ObjectId,
3656        distance: Distance,
3657        new_ast: &mut ast::Node<ast::Program>,
3658    ) -> Result<AstNodeRef, KclError> {
3659        let sketch_id = sketch;
3660        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3661            [pt0, pt1] => [
3662                self.coincident_segment_to_ast(pt0, new_ast)?,
3663                self.coincident_segment_to_ast(pt1, new_ast)?,
3664            ],
3665            _ => {
3666                return Err(KclError::refactor(format!(
3667                    "Distance constraint must have exactly 2 points, got {}",
3668                    distance.points.len()
3669                )));
3670            }
3671        };
3672
3673        let arguments = match &distance.label_position {
3674            Some(label_position) => vec![ast::LabeledArg {
3675                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3676                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3677            }],
3678            None => Default::default(),
3679        };
3680
3681        // Create the distance() call.
3682        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3683            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3684            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3685                ast::ArrayExpression {
3686                    elements: vec![pt0_ast, pt1_ast],
3687                    digest: None,
3688                    non_code_meta: Default::default(),
3689                },
3690            )))),
3691            arguments,
3692            digest: None,
3693            non_code_meta: Default::default(),
3694        })));
3695        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3696            left: distance_call_ast,
3697            operator: ast::BinaryOperator::Eq,
3698            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3699                value: ast::LiteralValue::Number {
3700                    value: distance.distance.value,
3701                    suffix: distance.distance.units,
3702                },
3703                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3704                    KclError::refactor(format!(
3705                        "Could not format numeric suffix: {:?}",
3706                        distance.distance.units
3707                    ))
3708                })?,
3709                digest: None,
3710            }))),
3711            digest: None,
3712        })));
3713
3714        // Add the line to the AST of the sketch block.
3715        let (sketch_block_ref, _) = self.mutate_ast(
3716            new_ast,
3717            sketch_id,
3718            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3719        )?;
3720        Ok(sketch_block_ref)
3721    }
3722
3723    async fn add_angle(
3724        &mut self,
3725        sketch: ObjectId,
3726        angle: Angle,
3727        new_ast: &mut ast::Node<ast::Program>,
3728    ) -> Result<AstNodeRef, KclError> {
3729        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3730            return Err(KclError::refactor(format!(
3731                "Angle constraint must have exactly 2 lines, got {}",
3732                angle.lines.len()
3733            )));
3734        };
3735        let sketch_id = sketch;
3736
3737        // Map the runtime objects back to variable names.
3738        let line0_object = self
3739            .scene_graph
3740            .objects
3741            .get(l0_id.0)
3742            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3743        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3744            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3745        };
3746        let Segment::Line(_) = line0_segment else {
3747            return Err(KclError::refactor(format!(
3748                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3749            )));
3750        };
3751        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3752
3753        let line1_object = self
3754            .scene_graph
3755            .objects
3756            .get(l1_id.0)
3757            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3758        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3759            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3760        };
3761        let Segment::Line(_) = line1_segment else {
3762            return Err(KclError::refactor(format!(
3763                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3764            )));
3765        };
3766        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3767
3768        // Create the angle() call.
3769        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3770            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3771            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3772                ast::ArrayExpression {
3773                    elements: vec![l0_ast, l1_ast],
3774                    digest: None,
3775                    non_code_meta: Default::default(),
3776                },
3777            )))),
3778            arguments: Default::default(),
3779            digest: None,
3780            non_code_meta: Default::default(),
3781        })));
3782        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3783            left: angle_call_ast,
3784            operator: ast::BinaryOperator::Eq,
3785            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3786                value: ast::LiteralValue::Number {
3787                    value: angle.angle.value,
3788                    suffix: angle.angle.units,
3789                },
3790                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3791                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3792                })?,
3793                digest: None,
3794            }))),
3795            digest: None,
3796        })));
3797
3798        // Add the line to the AST of the sketch block.
3799        let (sketch_block_ref, _) = self.mutate_ast(
3800            new_ast,
3801            sketch_id,
3802            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3803        )?;
3804        Ok(sketch_block_ref)
3805    }
3806
3807    async fn add_tangent(
3808        &mut self,
3809        sketch: ObjectId,
3810        tangent: Tangent,
3811        new_ast: &mut ast::Node<ast::Program>,
3812    ) -> Result<AstNodeRef, KclError> {
3813        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3814            return Err(KclError::refactor(format!(
3815                "Tangent constraint must have exactly 2 segments, got {}",
3816                tangent.input.len()
3817            )));
3818        };
3819        let sketch_id = sketch;
3820
3821        let seg0_object = self
3822            .scene_graph
3823            .objects
3824            .get(seg0_id.0)
3825            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3826        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3827            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3828        };
3829        let seg0_ast = match seg0_segment {
3830            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3831                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3832            }
3833            _ => {
3834                return Err(KclError::refactor(format!(
3835                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3836                )));
3837            }
3838        };
3839
3840        let seg1_object = self
3841            .scene_graph
3842            .objects
3843            .get(seg1_id.0)
3844            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3845        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3846            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3847        };
3848        let seg1_ast = match seg1_segment {
3849            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3850                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3851            }
3852            _ => {
3853                return Err(KclError::refactor(format!(
3854                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3855                )));
3856            }
3857        };
3858
3859        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3860        let (sketch_block_ref, _) = self.mutate_ast(
3861            new_ast,
3862            sketch_id,
3863            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3864        )?;
3865        Ok(sketch_block_ref)
3866    }
3867
3868    async fn add_symmetric(
3869        &mut self,
3870        sketch: ObjectId,
3871        symmetric: Symmetric,
3872        new_ast: &mut ast::Node<ast::Program>,
3873    ) -> Result<AstNodeRef, KclError> {
3874        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3875            return Err(KclError::refactor(format!(
3876                "Symmetric constraint must have exactly 2 inputs, got {}",
3877                symmetric.input.len()
3878            )));
3879        };
3880        let sketch_id = sketch;
3881
3882        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3883        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3884        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3885
3886        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3887        let (sketch_block_ref, _) = self.mutate_ast(
3888            new_ast,
3889            sketch_id,
3890            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3891        )?;
3892        Ok(sketch_block_ref)
3893    }
3894
3895    async fn add_midpoint(
3896        &mut self,
3897        sketch: ObjectId,
3898        midpoint: Midpoint,
3899        new_ast: &mut ast::Node<ast::Program>,
3900    ) -> Result<AstNodeRef, KclError> {
3901        let sketch_id = sketch;
3902        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3903
3904        let segment_object = self
3905            .scene_graph
3906            .objects
3907            .get(midpoint.segment.0)
3908            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3909        let ObjectKind::Segment {
3910            segment: midpoint_segment,
3911        } = &segment_object.kind
3912        else {
3913            return Err(KclError::refactor(format!(
3914                "Object must be a segment, but it was {}",
3915                segment_object.kind.human_friendly_kind_with_article()
3916            )));
3917        };
3918        let segment_ast = match midpoint_segment {
3919            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3920            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3921            _ => {
3922                return Err(KclError::refactor(format!(
3923                    "Midpoint target must be a line or arc segment but it was {}",
3924                    midpoint_segment.human_friendly_kind_with_article()
3925                )));
3926            }
3927        };
3928
3929        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3930        let (sketch_block_ref, _) = self.mutate_ast(
3931            new_ast,
3932            sketch_id,
3933            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3934        )?;
3935        Ok(sketch_block_ref)
3936    }
3937
3938    async fn add_equal_radius(
3939        &mut self,
3940        sketch: ObjectId,
3941        equal_radius: EqualRadius,
3942        new_ast: &mut ast::Node<ast::Program>,
3943    ) -> Result<AstNodeRef, KclError> {
3944        if equal_radius.input.len() < 2 {
3945            return Err(KclError::refactor(format!(
3946                "equalRadius constraint must have at least 2 segments, got {}",
3947                equal_radius.input.len()
3948            )));
3949        }
3950
3951        let sketch_id = sketch;
3952        let input_asts = equal_radius
3953            .input
3954            .iter()
3955            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3956            .collect::<Result<Vec<_>, _>>()?;
3957
3958        let equal_radius_ast = create_equal_radius_ast(input_asts);
3959        let (sketch_block_ref, _) = self.mutate_ast(
3960            new_ast,
3961            sketch_id,
3962            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3963        )?;
3964        Ok(sketch_block_ref)
3965    }
3966
3967    async fn add_radius(
3968        &mut self,
3969        sketch: ObjectId,
3970        radius: Radius,
3971        new_ast: &mut ast::Node<ast::Program>,
3972    ) -> Result<AstNodeRef, KclError> {
3973        let params = ArcSizeConstraintParams {
3974            points: vec![radius.arc],
3975            function_name: RADIUS_FN,
3976            value: radius.radius.value,
3977            units: radius.radius.units,
3978            label_position: radius.label_position,
3979            constraint_type_name: "Radius",
3980        };
3981        self.add_arc_size_constraint(sketch, params, new_ast).await
3982    }
3983
3984    async fn add_diameter(
3985        &mut self,
3986        sketch: ObjectId,
3987        diameter: Diameter,
3988        new_ast: &mut ast::Node<ast::Program>,
3989    ) -> Result<AstNodeRef, KclError> {
3990        let params = ArcSizeConstraintParams {
3991            points: vec![diameter.arc],
3992            function_name: DIAMETER_FN,
3993            value: diameter.diameter.value,
3994            units: diameter.diameter.units,
3995            label_position: diameter.label_position,
3996            constraint_type_name: "Diameter",
3997        };
3998        self.add_arc_size_constraint(sketch, params, new_ast).await
3999    }
4000
4001    async fn add_fixed_constraints(
4002        &mut self,
4003        sketch: ObjectId,
4004        points: Vec<FixedPoint>,
4005        new_ast: &mut ast::Node<ast::Program>,
4006    ) -> Result<AstNodeRef, KclError> {
4007        let mut sketch_block_ref = None;
4008
4009        for fixed_point in points {
4010            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4011            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4012                .map_err(|err| KclError::refactor(err.to_string()))?;
4013
4014            let (sketch_ref, _) = self.mutate_ast(
4015                new_ast,
4016                sketch,
4017                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4018            )?;
4019            sketch_block_ref = Some(sketch_ref);
4020        }
4021
4022        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4023    }
4024
4025    async fn add_arc_size_constraint(
4026        &mut self,
4027        sketch: ObjectId,
4028        params: ArcSizeConstraintParams,
4029        new_ast: &mut ast::Node<ast::Program>,
4030    ) -> Result<AstNodeRef, KclError> {
4031        let sketch_id = sketch;
4032
4033        // Constraint must have exactly 1 argument (arc segment)
4034        if params.points.len() != 1 {
4035            return Err(KclError::refactor(format!(
4036                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4037                params.constraint_type_name,
4038                params.points.len()
4039            )));
4040        }
4041
4042        let arc_id = params.points[0];
4043        let arc_object = self
4044            .scene_graph
4045            .objects
4046            .get(arc_id.0)
4047            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4048        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4049            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4050        };
4051        let ref_type = match arc_segment {
4052            Segment::Arc(_) => ARC_VARIABLE,
4053            Segment::Circle(_) => CIRCLE_VARIABLE,
4054            _ => {
4055                return Err(KclError::refactor(format!(
4056                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4057                    params.constraint_type_name
4058                )));
4059            }
4060        };
4061        // Reference the arc/circle segment directly
4062        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4063        let arguments = match &params.label_position {
4064            Some(label_position) => vec![ast::LabeledArg {
4065                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4066                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4067            }],
4068            None => Default::default(),
4069        };
4070
4071        // Create the function call.
4072        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4073            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4074            unlabeled: Some(arc_ast),
4075            arguments,
4076            digest: None,
4077            non_code_meta: Default::default(),
4078        })));
4079        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4080            left: call_ast,
4081            operator: ast::BinaryOperator::Eq,
4082            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4083                value: ast::LiteralValue::Number {
4084                    value: params.value,
4085                    suffix: params.units,
4086                },
4087                raw: format_number_literal(params.value, params.units, None)
4088                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4089                digest: None,
4090            }))),
4091            digest: None,
4092        })));
4093
4094        // Add the line to the AST of the sketch block.
4095        let (sketch_block_ref, _) = self.mutate_ast(
4096            new_ast,
4097            sketch_id,
4098            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4099        )?;
4100        Ok(sketch_block_ref)
4101    }
4102
4103    async fn add_horizontal_distance(
4104        &mut self,
4105        sketch: ObjectId,
4106        distance: Distance,
4107        new_ast: &mut ast::Node<ast::Program>,
4108    ) -> Result<AstNodeRef, KclError> {
4109        let sketch_id = sketch;
4110        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4111            [pt0, pt1] => [
4112                self.coincident_segment_to_ast(pt0, new_ast)?,
4113                self.coincident_segment_to_ast(pt1, new_ast)?,
4114            ],
4115            _ => {
4116                return Err(KclError::refactor(format!(
4117                    "Horizontal distance constraint must have exactly 2 points, got {}",
4118                    distance.points.len()
4119                )));
4120            }
4121        };
4122
4123        let arguments = match &distance.label_position {
4124            Some(label_position) => vec![ast::LabeledArg {
4125                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4126                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4127            }],
4128            None => Default::default(),
4129        };
4130
4131        // Create the horizontalDistance() call.
4132        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4133            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4134            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4135                ast::ArrayExpression {
4136                    elements: vec![pt0_ast, pt1_ast],
4137                    digest: None,
4138                    non_code_meta: Default::default(),
4139                },
4140            )))),
4141            arguments,
4142            digest: None,
4143            non_code_meta: Default::default(),
4144        })));
4145        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4146            left: distance_call_ast,
4147            operator: ast::BinaryOperator::Eq,
4148            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4149                value: ast::LiteralValue::Number {
4150                    value: distance.distance.value,
4151                    suffix: distance.distance.units,
4152                },
4153                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4154                    KclError::refactor(format!(
4155                        "Could not format numeric suffix: {:?}",
4156                        distance.distance.units
4157                    ))
4158                })?,
4159                digest: None,
4160            }))),
4161            digest: None,
4162        })));
4163
4164        // Add the line to the AST of the sketch block.
4165        let (sketch_block_ref, _) = self.mutate_ast(
4166            new_ast,
4167            sketch_id,
4168            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4169        )?;
4170        Ok(sketch_block_ref)
4171    }
4172
4173    async fn add_vertical_distance(
4174        &mut self,
4175        sketch: ObjectId,
4176        distance: Distance,
4177        new_ast: &mut ast::Node<ast::Program>,
4178    ) -> Result<AstNodeRef, KclError> {
4179        let sketch_id = sketch;
4180        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4181            [pt0, pt1] => [
4182                self.coincident_segment_to_ast(pt0, new_ast)?,
4183                self.coincident_segment_to_ast(pt1, new_ast)?,
4184            ],
4185            _ => {
4186                return Err(KclError::refactor(format!(
4187                    "Vertical distance constraint must have exactly 2 points, got {}",
4188                    distance.points.len()
4189                )));
4190            }
4191        };
4192
4193        let arguments = match &distance.label_position {
4194            Some(label_position) => vec![ast::LabeledArg {
4195                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4196                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4197            }],
4198            None => Default::default(),
4199        };
4200
4201        // Create the verticalDistance() call.
4202        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4203            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4204            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4205                ast::ArrayExpression {
4206                    elements: vec![pt0_ast, pt1_ast],
4207                    digest: None,
4208                    non_code_meta: Default::default(),
4209                },
4210            )))),
4211            arguments,
4212            digest: None,
4213            non_code_meta: Default::default(),
4214        })));
4215        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4216            left: distance_call_ast,
4217            operator: ast::BinaryOperator::Eq,
4218            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4219                value: ast::LiteralValue::Number {
4220                    value: distance.distance.value,
4221                    suffix: distance.distance.units,
4222                },
4223                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4224                    KclError::refactor(format!(
4225                        "Could not format numeric suffix: {:?}",
4226                        distance.distance.units
4227                    ))
4228                })?,
4229                digest: None,
4230            }))),
4231            digest: None,
4232        })));
4233
4234        // Add the line to the AST of the sketch block.
4235        let (sketch_block_ref, _) = self.mutate_ast(
4236            new_ast,
4237            sketch_id,
4238            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4239        )?;
4240        Ok(sketch_block_ref)
4241    }
4242
4243    async fn add_horizontal(
4244        &mut self,
4245        sketch: ObjectId,
4246        horizontal: Horizontal,
4247        new_ast: &mut ast::Node<ast::Program>,
4248    ) -> Result<AstNodeRef, KclError> {
4249        let sketch_id = sketch;
4250
4251        // Map the runtime objects back to variable names.
4252        let first_arg_ast = match horizontal {
4253            Horizontal::Line { line } => {
4254                let line_object = self
4255                    .scene_graph
4256                    .objects
4257                    .get(line.0)
4258                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4259                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4260                    let kind = line_object.kind.human_friendly_kind_with_article();
4261                    return Err(KclError::refactor(format!(
4262                        "This constraint only works on Segments, but you selected {kind}"
4263                    )));
4264                };
4265                let Segment::Line(_) = line_segment else {
4266                    return Err(KclError::refactor(format!(
4267                        "Only lines can be made horizontal, but you selected {}",
4268                        line_segment.human_friendly_kind_with_article(),
4269                    )));
4270                };
4271                self.line_id_to_ast_reference(line, new_ast)?
4272            }
4273            Horizontal::Points { points } => {
4274                let point_asts = points
4275                    .iter()
4276                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4277                    .collect::<Result<Vec<_>, _>>()?;
4278                ast::ArrayExpression::new(point_asts).into()
4279            }
4280        };
4281        // Create the horizontal() call using shared helper.
4282        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4283
4284        // Add the line to the AST of the sketch block.
4285        let (sketch_block_ref, _) = self.mutate_ast(
4286            new_ast,
4287            sketch_id,
4288            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4289        )?;
4290        Ok(sketch_block_ref)
4291    }
4292
4293    async fn add_lines_equal_length(
4294        &mut self,
4295        sketch: ObjectId,
4296        lines_equal_length: LinesEqualLength,
4297        new_ast: &mut ast::Node<ast::Program>,
4298    ) -> Result<AstNodeRef, KclError> {
4299        if lines_equal_length.lines.len() < 2 {
4300            return Err(KclError::refactor(format!(
4301                "Lines equal length constraint must have at least 2 lines, got {}",
4302                lines_equal_length.lines.len()
4303            )));
4304        };
4305
4306        let sketch_id = sketch;
4307
4308        // Map the runtime objects back to variable names.
4309        let line_asts = lines_equal_length
4310            .lines
4311            .iter()
4312            .map(|line_id| {
4313                let line_object = self
4314                    .scene_graph
4315                    .objects
4316                    .get(line_id.0)
4317                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4318                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4319                    let kind = line_object.kind.human_friendly_kind_with_article();
4320                    return Err(KclError::refactor(format!(
4321                        "This constraint only works on Segments, but you selected {kind}"
4322                    )));
4323                };
4324                let Segment::Line(_) = line_segment else {
4325                    let kind = line_segment.human_friendly_kind_with_article();
4326                    return Err(KclError::refactor(format!(
4327                        "Only lines can be made equal length, but you selected {kind}"
4328                    )));
4329                };
4330
4331                self.line_id_to_ast_reference(*line_id, new_ast)
4332            })
4333            .collect::<Result<Vec<_>, _>>()?;
4334
4335        // Create the equalLength() call using shared helper.
4336        let equal_length_ast = create_equal_length_ast(line_asts);
4337
4338        // Add the constraint to the AST of the sketch block.
4339        let (sketch_block_ref, _) = self.mutate_ast(
4340            new_ast,
4341            sketch_id,
4342            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4343        )?;
4344        Ok(sketch_block_ref)
4345    }
4346
4347    fn equal_radius_segment_id_to_ast_reference(
4348        &mut self,
4349        segment_id: ObjectId,
4350        new_ast: &mut ast::Node<ast::Program>,
4351    ) -> Result<ast::Expr, KclError> {
4352        let segment_object = self
4353            .scene_graph
4354            .objects
4355            .get(segment_id.0)
4356            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4357        let ObjectKind::Segment { segment } = &segment_object.kind else {
4358            return Err(KclError::refactor(format!(
4359                "Object is not a segment, it was {}",
4360                segment_object.kind.human_friendly_kind_with_article()
4361            )));
4362        };
4363
4364        let ref_type = match segment {
4365            Segment::Arc(_) => ARC_VARIABLE,
4366            Segment::Circle(_) => CIRCLE_VARIABLE,
4367            _ => {
4368                return Err(KclError::refactor(format!(
4369                    "equalRadius supports only arc/circle segments, got {}",
4370                    segment.human_friendly_kind_with_article()
4371                )));
4372            }
4373        };
4374
4375        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4376    }
4377
4378    fn symmetric_input_id_to_ast_reference(
4379        &mut self,
4380        segment_id: ObjectId,
4381        new_ast: &mut ast::Node<ast::Program>,
4382    ) -> Result<ast::Expr, KclError> {
4383        let segment_object = self
4384            .scene_graph
4385            .objects
4386            .get(segment_id.0)
4387            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4388        let ObjectKind::Segment { segment } = &segment_object.kind else {
4389            return Err(KclError::refactor(format!(
4390                "Object is not a segment, it was {}",
4391                segment_object.kind.human_friendly_kind_with_article()
4392            )));
4393        };
4394
4395        match segment {
4396            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4397            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4398            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4399            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4400            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4401                "Symmetric does not yet support control point splines".to_owned(),
4402            )),
4403        }
4404    }
4405
4406    fn symmetric_axis_id_to_ast_reference(
4407        &mut self,
4408        segment_id: ObjectId,
4409        new_ast: &mut ast::Node<ast::Program>,
4410    ) -> Result<ast::Expr, KclError> {
4411        let segment_object = self
4412            .scene_graph
4413            .objects
4414            .get(segment_id.0)
4415            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4416        let ObjectKind::Segment { segment } = &segment_object.kind else {
4417            return Err(KclError::refactor(format!(
4418                "Object is not a segment, it was {}",
4419                segment_object.kind.human_friendly_kind_with_article()
4420            )));
4421        };
4422        match segment {
4423            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4424            _ => Err(KclError::refactor(format!(
4425                "Symmetric axis must be a line, got {}",
4426                segment.human_friendly_kind_with_article()
4427            ))),
4428        }
4429    }
4430
4431    async fn add_parallel(
4432        &mut self,
4433        sketch: ObjectId,
4434        parallel: Parallel,
4435        new_ast: &mut ast::Node<ast::Program>,
4436    ) -> Result<AstNodeRef, KclError> {
4437        if parallel.lines.len() < 2 {
4438            return Err(KclError::refactor(format!(
4439                "Parallel constraint must have at least 2 lines, got {}",
4440                parallel.lines.len()
4441            )));
4442        };
4443
4444        let sketch_id = sketch;
4445
4446        let line_asts = parallel
4447            .lines
4448            .iter()
4449            .map(|line_id| {
4450                let line_object = self
4451                    .scene_graph
4452                    .objects
4453                    .get(line_id.0)
4454                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4455                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4456                    let kind = line_object.kind.human_friendly_kind_with_article();
4457                    return Err(KclError::refactor(format!(
4458                        "This constraint only works on Segments, but you selected {kind}"
4459                    )));
4460                };
4461                let Segment::Line(_) = line_segment else {
4462                    let kind = line_segment.human_friendly_kind_with_article();
4463                    return Err(KclError::refactor(format!(
4464                        "Only lines can be made parallel, but you selected {kind}"
4465                    )));
4466                };
4467
4468                self.line_id_to_ast_reference(*line_id, new_ast)
4469            })
4470            .collect::<Result<Vec<_>, _>>()?;
4471
4472        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4473            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4474            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4475                ast::ArrayExpression {
4476                    elements: line_asts,
4477                    digest: None,
4478                    non_code_meta: Default::default(),
4479                },
4480            )))),
4481            arguments: Default::default(),
4482            digest: None,
4483            non_code_meta: Default::default(),
4484        })));
4485
4486        let (sketch_block_ref, _) = self.mutate_ast(
4487            new_ast,
4488            sketch_id,
4489            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4490        )?;
4491        Ok(sketch_block_ref)
4492    }
4493
4494    async fn add_perpendicular(
4495        &mut self,
4496        sketch: ObjectId,
4497        perpendicular: Perpendicular,
4498        new_ast: &mut ast::Node<ast::Program>,
4499    ) -> Result<AstNodeRef, KclError> {
4500        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4501            .await
4502    }
4503
4504    async fn add_lines_at_angle_constraint(
4505        &mut self,
4506        sketch: ObjectId,
4507        angle_kind: LinesAtAngleKind,
4508        lines: Vec<ObjectId>,
4509        new_ast: &mut ast::Node<ast::Program>,
4510    ) -> Result<AstNodeRef, KclError> {
4511        let &[line0_id, line1_id] = lines.as_slice() else {
4512            return Err(KclError::refactor(format!(
4513                "{} constraint must have exactly 2 lines, got {}",
4514                angle_kind.to_function_name(),
4515                lines.len()
4516            )));
4517        };
4518
4519        let sketch_id = sketch;
4520
4521        // Map the runtime objects back to variable names.
4522        let line0_object = self
4523            .scene_graph
4524            .objects
4525            .get(line0_id.0)
4526            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4527        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4528            let kind = line0_object.kind.human_friendly_kind_with_article();
4529            return Err(KclError::refactor(format!(
4530                "This constraint only works on Segments, but you selected {kind}"
4531            )));
4532        };
4533        let Segment::Line(_) = line0_segment else {
4534            return Err(KclError::refactor(format!(
4535                "Only lines can be made {}, but you selected {}",
4536                angle_kind.to_function_name(),
4537                line0_segment.human_friendly_kind_with_article(),
4538            )));
4539        };
4540        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4541
4542        let line1_object = self
4543            .scene_graph
4544            .objects
4545            .get(line1_id.0)
4546            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4547        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4548            let kind = line1_object.kind.human_friendly_kind_with_article();
4549            return Err(KclError::refactor(format!(
4550                "This constraint only works on Segments, but you selected {kind}"
4551            )));
4552        };
4553        let Segment::Line(_) = line1_segment else {
4554            return Err(KclError::refactor(format!(
4555                "Only lines can be made {}, but you selected {}",
4556                angle_kind.to_function_name(),
4557                line1_segment.human_friendly_kind_with_article(),
4558            )));
4559        };
4560        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4561
4562        // Create the parallel() or perpendicular() call.
4563        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4564            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4565            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4566                ast::ArrayExpression {
4567                    elements: vec![line0_ast, line1_ast],
4568                    digest: None,
4569                    non_code_meta: Default::default(),
4570                },
4571            )))),
4572            arguments: Default::default(),
4573            digest: None,
4574            non_code_meta: Default::default(),
4575        })));
4576
4577        // Add the constraint to the AST of the sketch block.
4578        let (sketch_block_ref, _) = self.mutate_ast(
4579            new_ast,
4580            sketch_id,
4581            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4582        )?;
4583        Ok(sketch_block_ref)
4584    }
4585
4586    async fn add_vertical(
4587        &mut self,
4588        sketch: ObjectId,
4589        vertical: Vertical,
4590        new_ast: &mut ast::Node<ast::Program>,
4591    ) -> Result<AstNodeRef, KclError> {
4592        let sketch_id = sketch;
4593
4594        let first_arg_ast = match vertical {
4595            Vertical::Line { line } => {
4596                // Map the runtime objects back to variable names.
4597                let line_object = self
4598                    .scene_graph
4599                    .objects
4600                    .get(line.0)
4601                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4602                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4603                    let kind = line_object.kind.human_friendly_kind_with_article();
4604                    return Err(KclError::refactor(format!(
4605                        "This constraint only works on Segments, but you selected {kind}"
4606                    )));
4607                };
4608                let Segment::Line(_) = line_segment else {
4609                    return Err(KclError::refactor(format!(
4610                        "Only lines can be made vertical, but you selected {}",
4611                        line_segment.human_friendly_kind_with_article()
4612                    )));
4613                };
4614                self.line_id_to_ast_reference(line, new_ast)?
4615            }
4616            Vertical::Points { points } => {
4617                let point_asts = points
4618                    .iter()
4619                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4620                    .collect::<Result<Vec<_>, _>>()?;
4621                ast::ArrayExpression::new(point_asts).into()
4622            }
4623        };
4624        // Create the vertical() call using shared helper.
4625        let vertical_ast = create_vertical_ast(first_arg_ast);
4626
4627        // Add the line to the AST of the sketch block.
4628        let (sketch_block_ref, _) = self.mutate_ast(
4629            new_ast,
4630            sketch_id,
4631            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4632        )?;
4633        Ok(sketch_block_ref)
4634    }
4635
4636    async fn execute_after_add_constraint(
4637        &mut self,
4638        ctx: &ExecutorContext,
4639        sketch_id: ObjectId,
4640        sketch_block_ref: AstNodeRef,
4641        new_ast: &mut ast::Node<ast::Program>,
4642    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4643        // Convert to string source to create real source ranges.
4644        let new_source = source_from_ast(new_ast);
4645        // Parse the new KCL source.
4646        let (new_program, errors) = Program::parse(&new_source)
4647            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4648        if !errors.is_empty() {
4649            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4650                "Error parsing KCL source after adding constraint: {errors:?}"
4651            ))));
4652        }
4653        let Some(new_program) = new_program else {
4654            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4655                "No AST produced after adding constraint".to_string(),
4656            )));
4657        };
4658        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4659            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4660                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4661            )))
4662        })?;
4663
4664        // Truncate after the sketch block for mock execution.
4665        // Use a clone so we don't mutate new_program yet
4666        let mut truncated_program = new_program.clone();
4667        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4668            .map_err(KclErrorWithOutputs::no_outputs)?;
4669
4670        // Execute - if this fails, we haven't modified self yet, so state is safe
4671        let outcome = ctx
4672            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4673            .await?;
4674
4675        let new_object_ids = {
4676            // Extract the constraint ID from the execution outcome using source_range_to_object
4677            let constraint_id = outcome
4678                .source_range_to_object
4679                .get(&constraint_node_ref.range)
4680                .copied()
4681                .ok_or_else(|| {
4682                    KclErrorWithOutputs::from_error_outcome(
4683                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4684                        outcome.clone(),
4685                    )
4686                })?;
4687            vec![constraint_id]
4688        };
4689
4690        // Only now, after all operations succeeded, update self.program.
4691        // This ensures state is only modified if everything succeeds.
4692        self.program = new_program;
4693
4694        // Uses MockConfig::default() which has freedom_analysis: true
4695        let outcome = self.update_state_after_exec(outcome, true);
4696
4697        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4698        let scene_graph_delta = SceneGraphDelta {
4699            new_graph: self.scene_graph_for_ui(),
4700            invalidates_ids: false,
4701            new_objects: new_object_ids,
4702            exec_outcome: outcome,
4703        };
4704        Ok((src_delta, scene_graph_delta))
4705    }
4706
4707    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4708        let commit_failure = || {
4709            KclErrorWithOutputs::from_error_outcome(
4710                KclError::refactor(format!("Could not update KCL after {operation}.")),
4711                outcome.clone(),
4712            )
4713        };
4714
4715        let default_length_unit = self.default_length_unit();
4716        let mut settled_ast = self.program.ast.clone();
4717        let mut committed_solver_value = false;
4718        for (var_range, node_path, value) in &outcome.var_solutions {
4719            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4720                return Err(commit_failure());
4721            };
4722            let new_value = match &lookup {
4723                Some(current_literal) => {
4724                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4725                        continue;
4726                    }
4727                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4728                }
4729                None => {
4730                    // Bare `var` with no initial literal to compare against;
4731                    // always commit, using the module's default length unit as
4732                    // an explicit suffix so the written value carries units.
4733                    Number {
4734                        value: number_value_in_default_length_units(*value, default_length_unit),
4735                        units: default_length_unit.into(),
4736                    }
4737                }
4738            };
4739            committed_solver_value = true;
4740            let source_ref = SourceRef::Simple {
4741                range: *var_range,
4742                node_path: node_path.clone(),
4743            };
4744            mutate_ast_node_by_source_ref(
4745                &mut settled_ast,
4746                &source_ref,
4747                AstMutateCommand::EditVarInitialValue { value: new_value },
4748            )
4749            .map_err(|_| commit_failure())?;
4750        }
4751
4752        if !committed_solver_value {
4753            return Ok(SourceDelta {
4754                text: self.program.original_file_contents.clone(),
4755            });
4756        }
4757
4758        let settled_source = source_from_ast(&settled_ast);
4759        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4760        if !errors.is_empty() {
4761            return Err(commit_failure());
4762        }
4763        let Some(settled_program) = settled_program else {
4764            return Err(commit_failure());
4765        };
4766
4767        self.program = settled_program;
4768
4769        Ok(SourceDelta { text: settled_source })
4770    }
4771
4772    // Find constraints that reference the given segments.
4773    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4774        if segment_ids_set.contains(&segment_id) {
4775            return true;
4776        }
4777
4778        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4779            return false;
4780        };
4781        let ObjectKind::Segment { segment } = &segment_object.kind else {
4782            return false;
4783        };
4784        let Segment::Point(point) = segment else {
4785            return false;
4786        };
4787
4788        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4789    }
4790
4791    fn remaining_constraint_segments(
4792        &self,
4793        segments: &[ConstraintSegment],
4794        segment_ids_set: &AhashIndexSet<ObjectId>,
4795    ) -> Vec<ConstraintSegment> {
4796        segments
4797            .iter()
4798            .copied()
4799            .filter(|segment| match segment {
4800                ConstraintSegment::Origin(_) => true,
4801                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4802            })
4803            .collect()
4804    }
4805
4806    fn find_referenced_constraints(
4807        &self,
4808        sketch_id: ObjectId,
4809        segment_ids_set: &AhashIndexSet<ObjectId>,
4810    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4811        // Look up the sketch.
4812        let sketch_object = self
4813            .scene_graph
4814            .objects
4815            .get(sketch_id.0)
4816            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4817        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4818            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4819        };
4820        let segment_or_owner_matches = |segment_id: ObjectId| {
4821            if segment_ids_set.contains(&segment_id) {
4822                return true;
4823            }
4824            let segment_object = self.scene_graph.objects.get(segment_id.0);
4825            if let Some(obj) = segment_object
4826                && let ObjectKind::Segment { segment } = &obj.kind
4827            {
4828                match segment {
4829                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4830                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4831                    _ => false,
4832                }
4833            } else {
4834                false
4835            }
4836        };
4837        let mut constraint_ids_set = AhashIndexSet::default();
4838        for constraint_id in &sketch.constraints {
4839            let constraint_object = self
4840                .scene_graph
4841                .objects
4842                .get(constraint_id.0)
4843                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4844            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4845                return Err(KclError::refactor(format!(
4846                    "Object is not a constraint, it is {}",
4847                    constraint_object.kind.human_friendly_kind_with_article()
4848                )));
4849            };
4850            let depends_on_segment = match constraint {
4851                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4852                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4853                Constraint::Fixed(fixed) => fixed
4854                    .points
4855                    .iter()
4856                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4857                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4858                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4859                Constraint::EqualRadius(equal_radius) => {
4860                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4861                }
4862                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4863                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4864                Constraint::Horizontal(h) => match h {
4865                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4866                    Horizontal::Points { points } => points.iter().any(|point| match point {
4867                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4868                        ConstraintSegment::Origin(_) => false,
4869                    }),
4870                },
4871                Constraint::Vertical(v) => match v {
4872                    Vertical::Line { line } => segment_or_owner_matches(*line),
4873                    Vertical::Points { points } => points.iter().any(|point| match point {
4874                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4875                        ConstraintSegment::Origin(_) => false,
4876                    }),
4877                },
4878                Constraint::LinesEqualLength(lines_equal_length) => {
4879                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4880                }
4881                Constraint::Midpoint(midpoint) => {
4882                    segment_or_owner_matches(midpoint.segment)
4883                        || matches!(
4884                            midpoint.point,
4885                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4886                        )
4887                }
4888                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4889                Constraint::Perpendicular(perpendicular) => {
4890                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4891                }
4892                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4893                Constraint::Symmetric(symmetric) => {
4894                    segment_or_owner_matches(symmetric.axis)
4895                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4896                }
4897                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4898            };
4899            if depends_on_segment {
4900                constraint_ids_set.insert(*constraint_id);
4901            }
4902        }
4903        Ok(constraint_ids_set)
4904    }
4905
4906    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4907        let mut outcome = outcome;
4908        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4909
4910        if freedom_analysis_ran {
4911            // When freedom analysis ran, replace the cache entirely with new values
4912            // Don't merge with old values since IDs might have changed
4913            self.point_freedom_cache.clear();
4914            for new_obj in &new_objects {
4915                if let ObjectKind::Segment {
4916                    segment: crate::front::Segment::Point(point),
4917                } = &new_obj.kind
4918                {
4919                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4920                }
4921            }
4922            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4923            // Objects are already correct from the analysis, just use them as-is
4924            self.scene_graph.objects = new_objects;
4925        } else {
4926            // When freedom analysis didn't run, preserve old values and merge
4927            // Before replacing objects, extract and store freedom values from old objects
4928            for old_obj in &self.scene_graph.objects {
4929                if let ObjectKind::Segment {
4930                    segment: crate::front::Segment::Point(point),
4931                } = &old_obj.kind
4932                {
4933                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4934                }
4935            }
4936
4937            // Update objects, preserving stored freedom values when new is Free (might be default)
4938            let mut updated_objects = Vec::with_capacity(new_objects.len());
4939            for new_obj in new_objects {
4940                let mut obj = new_obj;
4941                if let ObjectKind::Segment {
4942                    segment: crate::front::Segment::Point(point),
4943                } = &mut obj.kind
4944                {
4945                    let new_freedom = point.freedom;
4946                    // When freedom_analysis=false, new values are defaults (Free).
4947                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4948                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4949                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4950                    // when there are actually unsatisfied constraints.
4951                    match new_freedom {
4952                        Freedom::Free => {
4953                            match self.point_freedom_cache.get(&obj.id).copied() {
4954                                Some(Freedom::Conflict) => {
4955                                    // Don't preserve Conflict - conflicts are transient
4956                                    // Keep it as Free
4957                                }
4958                                Some(Freedom::Fixed) => {
4959                                    // Preserve Fixed cached value
4960                                    point.freedom = Freedom::Fixed;
4961                                }
4962                                Some(Freedom::Free) => {
4963                                    // If stored is also Free, keep Free (no change needed)
4964                                }
4965                                None => {
4966                                    // If no cached value, keep Free (default)
4967                                }
4968                            }
4969                        }
4970                        Freedom::Fixed => {
4971                            // Use new value (already set)
4972                        }
4973                        Freedom::Conflict => {
4974                            // Use new value (already set)
4975                        }
4976                    }
4977                    // Store the new freedom value (even if it's Free, so we know it was set)
4978                    self.point_freedom_cache.insert(obj.id, point.freedom);
4979                }
4980                updated_objects.push(obj);
4981            }
4982
4983            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4984            self.scene_graph.objects = updated_objects;
4985        }
4986        outcome
4987    }
4988
4989    fn mutate_ast(
4990        &mut self,
4991        ast: &mut ast::Node<ast::Program>,
4992        object_id: ObjectId,
4993        command: AstMutateCommand,
4994    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4995        let sketch_object = self
4996            .scene_graph
4997            .objects
4998            .get(object_id.0)
4999            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5000        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5001    }
5002}
5003
5004fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5005    // Look up existing sketch.
5006    let sketch_object = scene_graph
5007        .objects
5008        .get(sketch_id.0)
5009        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5010    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5011        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5012    };
5013    expect_single_node_ref(sketch_object)
5014}
5015
5016fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5017    match &object.source {
5018        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5019            range: *range,
5020            node_path: node_path.clone(),
5021        }),
5022        SourceRef::BackTrace { ranges } => {
5023            let [range] = ranges.as_slice() else {
5024                return Err(KclError::refactor(format!(
5025                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5026                    ranges.len()
5027                )));
5028            };
5029            Ok(AstNodeRef {
5030                range: range.0,
5031                node_path: range.1.clone(),
5032            })
5033        }
5034    }
5035}
5036
5037/// This is a deprecated fall-back implementation. Prefer
5038/// [`only_sketch_block()`] to avoid reliance on source ranges.
5039fn only_sketch_block_from_range(
5040    ast: &mut ast::Node<ast::Program>,
5041    sketch_block_range: SourceRange,
5042    edit_kind: ChangeKind,
5043) -> Result<(), KclError> {
5044    let r1 = sketch_block_range;
5045    let matches_range = |r2: SourceRange| -> bool {
5046        // We may have added items to the sketch block, so the end may not be an
5047        // exact match.
5048        match edit_kind {
5049            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5050            // For edit, we don't know whether it grew or shrank.
5051            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5052            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5053            // No edit should be an exact match.
5054            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5055        }
5056    };
5057    let mut found = false;
5058    for item in ast.body.iter_mut() {
5059        match item {
5060            ast::BodyItem::ImportStatement(_) => {}
5061            ast::BodyItem::ExpressionStatement(node) => {
5062                if matches_range(SourceRange::from(&*node))
5063                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5064                {
5065                    sketch_block.is_being_edited = true;
5066                    found = true;
5067                    break;
5068                }
5069            }
5070            ast::BodyItem::VariableDeclaration(node) => {
5071                if matches_range(SourceRange::from(&node.declaration.init))
5072                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5073                {
5074                    sketch_block.is_being_edited = true;
5075                    found = true;
5076                    break;
5077                }
5078            }
5079            ast::BodyItem::TypeDeclaration(_) => {}
5080            ast::BodyItem::ReturnStatement(node) => {
5081                if matches_range(SourceRange::from(&node.argument))
5082                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5083                {
5084                    sketch_block.is_being_edited = true;
5085                    found = true;
5086                    break;
5087                }
5088            }
5089        }
5090    }
5091    if !found {
5092        return Err(KclError::refactor(format!(
5093            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5094        )));
5095    }
5096
5097    Ok(())
5098}
5099
5100fn only_sketch_block(
5101    ast: &mut ast::Node<ast::Program>,
5102    sketch_block_ref: &AstNodeRef,
5103    edit_kind: ChangeKind,
5104) -> Result<(), KclError> {
5105    let Some(target_node_path) = &sketch_block_ref.node_path else {
5106        #[cfg(target_arch = "wasm32")]
5107        web_sys::console::warn_1(
5108            &format!(
5109                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5110                &sketch_block_ref
5111            )
5112            .into(),
5113        );
5114        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5115    };
5116    let mut found = false;
5117    for item in ast.body.iter_mut() {
5118        match item {
5119            ast::BodyItem::ImportStatement(_) => {}
5120            ast::BodyItem::ExpressionStatement(node) => {
5121                // Check the statement.
5122                if let Some(node_path) = &node.node_path
5123                    && node_path == target_node_path
5124                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5125                {
5126                    sketch_block.is_being_edited = true;
5127                    found = true;
5128                    break;
5129                }
5130                // Check the expression.
5131                if let Some(node_path) = node.expression.node_path()
5132                    && node_path == target_node_path
5133                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5134                {
5135                    sketch_block.is_being_edited = true;
5136                    found = true;
5137                    break;
5138                }
5139            }
5140            ast::BodyItem::VariableDeclaration(node) => {
5141                if let Some(node_path) = node.declaration.init.node_path()
5142                    && node_path == target_node_path
5143                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5144                {
5145                    sketch_block.is_being_edited = true;
5146                    found = true;
5147                    break;
5148                }
5149            }
5150            ast::BodyItem::TypeDeclaration(_) => {}
5151            ast::BodyItem::ReturnStatement(node) => {
5152                if let Some(node_path) = node.argument.node_path()
5153                    && node_path == target_node_path
5154                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5155                {
5156                    sketch_block.is_being_edited = true;
5157                    found = true;
5158                    break;
5159                }
5160            }
5161        }
5162    }
5163    if !found {
5164        return Err(KclError::refactor(format!(
5165            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5166        )));
5167    }
5168
5169    Ok(())
5170}
5171
5172fn sketch_on_ast_expr(
5173    ast: &mut ast::Node<ast::Program>,
5174    scene_graph: &SceneGraph,
5175    on: &Plane,
5176) -> Result<ast::Expr, KclError> {
5177    match on {
5178        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5179        Plane::Object(object_id) => {
5180            let on_object = scene_graph
5181                .objects
5182                .get(object_id.0)
5183                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5184            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5185                return Ok(face_expr);
5186            }
5187            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5188        }
5189    }
5190}
5191
5192fn sketch_face_of_scene_object_ast_expr(
5193    ast: &mut ast::Node<ast::Program>,
5194    on_object: &crate::front::Object,
5195) -> Result<Option<ast::Expr>, KclError> {
5196    let SourceRef::BackTrace { ranges } = &on_object.source else {
5197        return Ok(None);
5198    };
5199
5200    match &on_object.kind {
5201        ObjectKind::Wall(_) => {
5202            let [sweep_range, segment_range] = ranges.as_slice() else {
5203                return Err(KclError::refactor(format!(
5204                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5205                    ranges.len(),
5206                    on_object.artifact_id
5207                )));
5208            };
5209            let sweep_ref = get_or_insert_ast_reference(
5210                ast,
5211                &SourceRef::Simple {
5212                    range: sweep_range.0,
5213                    node_path: sweep_range.1.clone(),
5214                },
5215                "solid",
5216                None,
5217            )?;
5218            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5219                return Err(KclError::refactor(format!(
5220                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5221                    on_object.artifact_id
5222                )));
5223            };
5224            let solid_name = solid_name_expr.name.name.clone();
5225            let solid_expr = ast_name_expr(solid_name.clone());
5226            let segment_ref = get_or_insert_ast_reference(
5227                ast,
5228                &SourceRef::Simple {
5229                    range: segment_range.0,
5230                    node_path: segment_range.1.clone(),
5231                },
5232                LINE_VARIABLE,
5233                None,
5234            )?;
5235
5236            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5237                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5238                    return Err(KclError::refactor(format!(
5239                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5240                        on_object.artifact_id
5241                    )));
5242                };
5243                create_member_expression(
5244                    create_member_expression(ast_name_expr(region_name), "tags"),
5245                    &segment_name_expr.name.name,
5246                )
5247            } else {
5248                segment_ref
5249            };
5250
5251            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5252        }
5253        ObjectKind::Cap(cap) => {
5254            let [range] = ranges.as_slice() else {
5255                return Err(KclError::refactor(format!(
5256                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5257                    ranges.len(),
5258                    on_object.artifact_id
5259                )));
5260            };
5261            let sweep_ref = get_or_insert_ast_reference(
5262                ast,
5263                &SourceRef::Simple {
5264                    range: range.0,
5265                    node_path: range.1.clone(),
5266                },
5267                "solid",
5268                None,
5269            )?;
5270            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5271                return Err(KclError::refactor(format!(
5272                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5273                    on_object.artifact_id
5274                )));
5275            };
5276            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5277            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5278            let face_expr = match cap.kind {
5279                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5280                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5281            };
5282
5283            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5284        }
5285        _ => Ok(None),
5286    }
5287}
5288
5289fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5290    let mut existing_artifact_ids = scene_objects
5291        .iter()
5292        .map(|object| object.artifact_id)
5293        .collect::<HashSet<_>>();
5294
5295    for artifact in artifact_graph.values() {
5296        match artifact {
5297            Artifact::Wall(wall) => {
5298                if existing_artifact_ids.contains(&wall.id) {
5299                    continue;
5300                }
5301
5302                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5303                    Artifact::Segment(segment) => Some(segment),
5304                    _ => None,
5305                }) else {
5306                    continue;
5307                };
5308                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5309                    Artifact::Sweep(sweep) => Some(sweep),
5310                    _ => None,
5311                }) else {
5312                    continue;
5313                };
5314                let source_segment = segment
5315                    .original_seg_id
5316                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5317                    .and_then(|artifact| match artifact {
5318                        Artifact::Segment(segment) => Some(segment),
5319                        _ => None,
5320                    })
5321                    .unwrap_or(segment);
5322                let id = ObjectId(scene_objects.len());
5323                scene_objects.push(crate::front::Object {
5324                    id,
5325                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5326                    label: Default::default(),
5327                    comments: Default::default(),
5328                    artifact_id: wall.id,
5329                    source: SourceRef::BackTrace {
5330                        ranges: vec![
5331                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5332                            (
5333                                source_segment.code_ref.range,
5334                                Some(source_segment.code_ref.node_path.clone()),
5335                            ),
5336                        ],
5337                    },
5338                });
5339                existing_artifact_ids.insert(wall.id);
5340            }
5341            Artifact::Cap(cap) => {
5342                if existing_artifact_ids.contains(&cap.id) {
5343                    continue;
5344                }
5345
5346                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5347                    Artifact::Sweep(sweep) => Some(sweep),
5348                    _ => None,
5349                }) else {
5350                    continue;
5351                };
5352                let id = ObjectId(scene_objects.len());
5353                let kind = match cap.sub_type {
5354                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5355                    CapSubType::End => crate::frontend::api::CapKind::End,
5356                };
5357                scene_objects.push(crate::front::Object {
5358                    id,
5359                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5360                    label: Default::default(),
5361                    comments: Default::default(),
5362                    artifact_id: cap.id,
5363                    source: SourceRef::BackTrace {
5364                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5365                    },
5366                });
5367                existing_artifact_ids.insert(cap.id);
5368            }
5369            _ => {}
5370        }
5371    }
5372}
5373
5374fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5375    use crate::engine::PlaneName;
5376
5377    match name {
5378        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5379        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5380        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5381        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5382        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5383        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5384    }
5385}
5386
5387fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5388    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5389        ast::UnaryOperator::Neg,
5390        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5391    )))
5392}
5393
5394fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5395    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5396        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5397        unlabeled: Some(solid_expr),
5398        arguments: vec![ast::LabeledArg {
5399            label: Some(ast::Identifier::new("face")),
5400            arg: face_expr,
5401        }],
5402        digest: None,
5403        non_code_meta: Default::default(),
5404    })))
5405}
5406
5407fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5408    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5409        return None;
5410    };
5411    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5412        return None;
5413    };
5414    if !matches!(
5415        sweep_call.callee.name.name.as_str(),
5416        "extrude" | "revolve" | "sweep" | "loft"
5417    ) {
5418        return None;
5419    }
5420    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5421        return None;
5422    };
5423    let candidate = region_name_expr.name.name.clone();
5424    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5425        return None;
5426    };
5427    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5428        return None;
5429    };
5430    if region_call.callee.name.name != "region" {
5431        return None;
5432    }
5433    Some(candidate)
5434}
5435
5436/// Return the AST expression referencing the variable at the given source ref.
5437/// If no such variable exists, insert a new variable declaration with the given
5438/// prefix.
5439///
5440/// This may return a complex expression referencing properties of the variable
5441/// (e.g., `line1.start`).
5442fn get_or_insert_ast_reference(
5443    ast: &mut ast::Node<ast::Program>,
5444    source_ref: &SourceRef,
5445    prefix: &str,
5446    property: Option<&str>,
5447) -> Result<ast::Expr, KclError> {
5448    let command = AstMutateCommand::AddVariableDeclaration {
5449        prefix: prefix.to_owned(),
5450    };
5451    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5452    let AstMutateCommandReturn::Name(var_name) = ret else {
5453        return Err(KclError::refactor(
5454            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5455        ));
5456    };
5457    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5458    let Some(property) = property else {
5459        // No property; just return the variable name.
5460        return Ok(var_expr);
5461    };
5462
5463    Ok(create_member_expression(var_expr, property))
5464}
5465
5466fn mutate_ast_node_by_source_ref(
5467    ast: &mut ast::Node<ast::Program>,
5468    source_ref: &SourceRef,
5469    command: AstMutateCommand,
5470) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5471    let (source_range, node_path) = match source_ref {
5472        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5473        SourceRef::BackTrace { ranges } => {
5474            let [range] = ranges.as_slice() else {
5475                return Err(KclError::refactor(format!(
5476                    "Expected single source ref, got {}; ranges={ranges:#?}",
5477                    ranges.len(),
5478                )));
5479            };
5480            (range.0, range.1.clone())
5481        }
5482    };
5483    let mut context = AstMutateContext {
5484        source_range,
5485        node_path,
5486        command,
5487        defined_names_stack: Default::default(),
5488    };
5489    let control = dfs_mut(ast, &mut context);
5490    match control {
5491        ControlFlow::Continue(_) => Err(KclError::refactor(
5492            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5493        )),
5494        ControlFlow::Break(break_value) => break_value,
5495    }
5496}
5497
5498#[derive(Debug)]
5499struct AstMutateContext {
5500    source_range: SourceRange,
5501    node_path: Option<ast::NodePath>,
5502    command: AstMutateCommand,
5503    defined_names_stack: Vec<HashSet<String>>,
5504}
5505
5506#[derive(Debug)]
5507#[allow(clippy::large_enum_variant)]
5508enum AstMutateCommand {
5509    /// Add an expression statement to the sketch block.
5510    AddSketchBlockExprStmt {
5511        expr: ast::Expr,
5512    },
5513    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5514    AddSketchBlockVarDecl {
5515        prefix: String,
5516        expr: ast::Expr,
5517    },
5518    AddVariableDeclaration {
5519        prefix: String,
5520    },
5521    EditPoint {
5522        at: ast::Expr,
5523    },
5524    EditLine {
5525        start: ast::Expr,
5526        end: ast::Expr,
5527        construction: Option<bool>,
5528    },
5529    EditArc {
5530        start: ast::Expr,
5531        end: ast::Expr,
5532        center: ast::Expr,
5533        construction: Option<bool>,
5534    },
5535    EditCircle {
5536        start: ast::Expr,
5537        center: ast::Expr,
5538        construction: Option<bool>,
5539    },
5540    EditControlPointSpline {
5541        points: ast::Expr,
5542        construction: Option<bool>,
5543    },
5544    EditConstraintValue {
5545        value: ast::BinaryPart,
5546    },
5547    EditDistanceConstraintLabelPosition {
5548        label_position: ast::Expr,
5549    },
5550    EditCallUnlabeled {
5551        arg: ast::Expr,
5552    },
5553    EditVarInitialValue {
5554        value: Number,
5555    },
5556    DeleteNode,
5557}
5558
5559impl AstMutateCommand {
5560    fn needs_defined_names_stack(&self) -> bool {
5561        matches!(
5562            self,
5563            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5564        )
5565    }
5566}
5567
5568#[derive(Debug)]
5569enum AstMutateCommandReturn {
5570    None,
5571    Name(String),
5572}
5573
5574#[derive(Debug, Clone)]
5575struct AstNodeRef {
5576    range: SourceRange,
5577    node_path: Option<ast::NodePath>,
5578}
5579
5580impl<T> From<&ast::Node<T>> for AstNodeRef {
5581    fn from(value: &ast::Node<T>) -> Self {
5582        AstNodeRef {
5583            range: value.into(),
5584            node_path: value.node_path.clone(),
5585        }
5586    }
5587}
5588
5589impl From<&ast::BodyItem> for AstNodeRef {
5590    fn from(value: &ast::BodyItem) -> Self {
5591        match value {
5592            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5593                range: node.into(),
5594                node_path: node.node_path.clone(),
5595            },
5596            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5597                range: node.into(),
5598                node_path: node.node_path.clone(),
5599            },
5600            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5601                range: node.into(),
5602                node_path: node.node_path.clone(),
5603            },
5604            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5605                range: node.into(),
5606                node_path: node.node_path.clone(),
5607            },
5608            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5609                range: node.into(),
5610                node_path: node.node_path.clone(),
5611            },
5612        }
5613    }
5614}
5615
5616impl From<&ast::Expr> for AstNodeRef {
5617    fn from(value: &ast::Expr) -> Self {
5618        AstNodeRef {
5619            range: SourceRange::from(value),
5620            node_path: value.node_path().cloned(),
5621        }
5622    }
5623}
5624
5625impl From<&AstMutateContext> for AstNodeRef {
5626    fn from(value: &AstMutateContext) -> Self {
5627        AstNodeRef {
5628            range: value.source_range,
5629            node_path: value.node_path.clone(),
5630        }
5631    }
5632}
5633
5634impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5635    type Error = crate::walk::AstNodeError;
5636
5637    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5638        Ok(AstNodeRef {
5639            range: SourceRange::try_from(value)?,
5640            node_path: value.try_into()?,
5641        })
5642    }
5643}
5644
5645impl From<AstNodeRef> for SourceRange {
5646    fn from(value: AstNodeRef) -> Self {
5647        value.range
5648    }
5649}
5650
5651impl Visitor for AstMutateContext {
5652    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5653    type Continue = ();
5654
5655    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5656        filter_and_process(self, node)
5657    }
5658
5659    fn finish(&mut self, node: NodeMut<'_>) {
5660        match &node {
5661            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5662                self.defined_names_stack.pop();
5663            }
5664            _ => {}
5665        }
5666    }
5667}
5668
5669fn filter_and_process(
5670    ctx: &mut AstMutateContext,
5671    node: NodeMut,
5672) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5673    let Ok(node_range) = SourceRange::try_from(&node) else {
5674        // Nodes that can't be converted to a range aren't interesting.
5675        return TraversalReturn::new_continue(());
5676    };
5677    // If we're adding a variable declaration, we need to look at variable
5678    // declaration expressions to see if it already has a variable, before
5679    // continuing. The variable declaration's source range won't match the
5680    // target; its init expression will.
5681    if let NodeMut::VariableDeclaration(var_decl) = &node {
5682        let expr_range = SourceRange::from(&var_decl.declaration.init);
5683        let expr_node_path = var_decl.declaration.init.node_path();
5684        if source_ref_matches(ctx, expr_range, expr_node_path) {
5685            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5686                // We found the variable declaration expression. It doesn't need
5687                // to be added.
5688                return TraversalReturn::new_break(Ok((
5689                    AstNodeRef::from(&**var_decl),
5690                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5691                )));
5692            }
5693            if let AstMutateCommand::DeleteNode = &ctx.command {
5694                // We found the variable declaration. Delete the variable along
5695                // with the segment.
5696                return TraversalReturn {
5697                    mutate_body_item: MutateBodyItem::Delete,
5698                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5699                };
5700            }
5701        }
5702    }
5703    // Similar thing with expression statement. We need to look at the
5704    // expression inside it.
5705    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5706        let expr_range = SourceRange::from(&expr_stmt.expression);
5707        let expr_node_path = expr_stmt.expression.node_path();
5708        if source_ref_matches(ctx, expr_range, expr_node_path) {
5709            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5710                // We found the node wrapped in an expression statement. Process
5711                // the statement.
5712                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5713                    return TraversalReturn::new_continue(());
5714                };
5715                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5716            }
5717            if let AstMutateCommand::DeleteNode = &ctx.command {
5718                // We found the node wrapped in an expression statement. Delete
5719                // the whole statement.
5720                return TraversalReturn {
5721                    mutate_body_item: MutateBodyItem::Delete,
5722                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5723                };
5724            }
5725        }
5726    }
5727
5728    if ctx.command.needs_defined_names_stack() {
5729        if let NodeMut::Program(program) = &node {
5730            ctx.defined_names_stack.push(find_defined_names(*program));
5731        } else if let NodeMut::SketchBlock(block) = &node {
5732            ctx.defined_names_stack.push(find_defined_names(&block.body));
5733        }
5734    }
5735
5736    // Make sure the node matches the source ref.
5737    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5738    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5739        return TraversalReturn::new_continue(());
5740    }
5741    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5742        return TraversalReturn::new_continue(());
5743    };
5744    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5745}
5746
5747fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5748    match &ctx.node_path {
5749        Some(target) => Some(target) == node_path,
5750        None => node_range == ctx.source_range,
5751    }
5752}
5753
5754fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5755    match &ctx.command {
5756        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5757            if let NodeMut::SketchBlock(sketch_block) = node {
5758                sketch_block
5759                    .body
5760                    .items
5761                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5762                        inner: ast::ExpressionStatement {
5763                            expression: expr.clone(),
5764                            digest: None,
5765                        },
5766                        start: Default::default(),
5767                        end: Default::default(),
5768                        module_id: Default::default(),
5769                        node_path: None,
5770                        outer_attrs: Default::default(),
5771                        pre_comments: Default::default(),
5772                        comment_start: Default::default(),
5773                    }));
5774                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5775            }
5776        }
5777        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5778            if let NodeMut::SketchBlock(sketch_block) = node {
5779                let empty_defined_names = HashSet::new();
5780                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5781                let Ok(name) = next_free_name(prefix, defined_names) else {
5782                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5783                };
5784                sketch_block
5785                    .body
5786                    .items
5787                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5788                        ast::VariableDeclaration::new(
5789                            ast::VariableDeclarator::new(&name, expr.clone()),
5790                            ast::ItemVisibility::Default,
5791                            ast::VariableKind::Const,
5792                        ),
5793                    ))));
5794                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5795            }
5796        }
5797        AstMutateCommand::AddVariableDeclaration { prefix } => {
5798            if let NodeMut::VariableDeclaration(inner) = node {
5799                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5800            }
5801            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5802                let empty_defined_names = HashSet::new();
5803                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5804                let Ok(name) = next_free_name(prefix, defined_names) else {
5805                    // TODO: Return an error instead?
5806                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5807                };
5808                let mutate_node =
5809                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5810                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5811                        ast::ItemVisibility::Default,
5812                        ast::VariableKind::Const,
5813                    ))));
5814                return TraversalReturn {
5815                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5816                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5817                };
5818            }
5819        }
5820        AstMutateCommand::EditPoint { at } => {
5821            if let NodeMut::CallExpressionKw(call) = node {
5822                if call.callee.name.name != POINT_FN {
5823                    return TraversalReturn::new_continue(());
5824                }
5825                // Update the arguments.
5826                for labeled_arg in &mut call.arguments {
5827                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5828                        labeled_arg.arg = at.clone();
5829                    }
5830                }
5831                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5832            }
5833        }
5834        AstMutateCommand::EditLine {
5835            start,
5836            end,
5837            construction,
5838        } => {
5839            if let NodeMut::CallExpressionKw(call) = node {
5840                if call.callee.name.name != LINE_FN {
5841                    return TraversalReturn::new_continue(());
5842                }
5843                // Update the arguments.
5844                for labeled_arg in &mut call.arguments {
5845                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5846                        labeled_arg.arg = start.clone();
5847                    }
5848                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5849                        labeled_arg.arg = end.clone();
5850                    }
5851                }
5852                // Handle construction kwarg
5853                if let Some(construction_value) = construction {
5854                    let construction_exists = call
5855                        .arguments
5856                        .iter()
5857                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5858                    if *construction_value {
5859                        // Add or update construction=true
5860                        if construction_exists {
5861                            // Update existing construction kwarg
5862                            for labeled_arg in &mut call.arguments {
5863                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5864                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5865                                        value: ast::LiteralValue::Bool(true),
5866                                        raw: "true".to_string(),
5867                                        digest: None,
5868                                    })));
5869                                }
5870                            }
5871                        } else {
5872                            // Add new construction kwarg
5873                            call.arguments.push(ast::LabeledArg {
5874                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5875                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5876                                    value: ast::LiteralValue::Bool(true),
5877                                    raw: "true".to_string(),
5878                                    digest: None,
5879                                }))),
5880                            });
5881                        }
5882                    } else {
5883                        // Remove construction kwarg if it exists
5884                        call.arguments
5885                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5886                    }
5887                }
5888                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5889            }
5890        }
5891        AstMutateCommand::EditArc {
5892            start,
5893            end,
5894            center,
5895            construction,
5896        } => {
5897            if let NodeMut::CallExpressionKw(call) = node {
5898                if call.callee.name.name != ARC_FN {
5899                    return TraversalReturn::new_continue(());
5900                }
5901                // Update the arguments.
5902                for labeled_arg in &mut call.arguments {
5903                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5904                        labeled_arg.arg = start.clone();
5905                    }
5906                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5907                        labeled_arg.arg = end.clone();
5908                    }
5909                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5910                        labeled_arg.arg = center.clone();
5911                    }
5912                }
5913                // Handle construction kwarg
5914                if let Some(construction_value) = construction {
5915                    let construction_exists = call
5916                        .arguments
5917                        .iter()
5918                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5919                    if *construction_value {
5920                        // Add or update construction=true
5921                        if construction_exists {
5922                            // Update existing construction kwarg
5923                            for labeled_arg in &mut call.arguments {
5924                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5925                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5926                                        value: ast::LiteralValue::Bool(true),
5927                                        raw: "true".to_string(),
5928                                        digest: None,
5929                                    })));
5930                                }
5931                            }
5932                        } else {
5933                            // Add new construction kwarg
5934                            call.arguments.push(ast::LabeledArg {
5935                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5936                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5937                                    value: ast::LiteralValue::Bool(true),
5938                                    raw: "true".to_string(),
5939                                    digest: None,
5940                                }))),
5941                            });
5942                        }
5943                    } else {
5944                        // Remove construction kwarg if it exists
5945                        call.arguments
5946                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5947                    }
5948                }
5949                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5950            }
5951        }
5952        AstMutateCommand::EditCircle {
5953            start,
5954            center,
5955            construction,
5956        } => {
5957            if let NodeMut::CallExpressionKw(call) = node {
5958                if call.callee.name.name != CIRCLE_FN {
5959                    return TraversalReturn::new_continue(());
5960                }
5961                // Update the arguments.
5962                for labeled_arg in &mut call.arguments {
5963                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5964                        labeled_arg.arg = start.clone();
5965                    }
5966                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5967                        labeled_arg.arg = center.clone();
5968                    }
5969                }
5970                // Handle construction kwarg
5971                if let Some(construction_value) = construction {
5972                    let construction_exists = call
5973                        .arguments
5974                        .iter()
5975                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5976                    if *construction_value {
5977                        if construction_exists {
5978                            // Update existing construction kwarg
5979                            for labeled_arg in &mut call.arguments {
5980                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5981                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5982                                        value: ast::LiteralValue::Bool(true),
5983                                        raw: "true".to_string(),
5984                                        digest: None,
5985                                    })));
5986                                }
5987                            }
5988                        } else {
5989                            // Add new construction kwarg
5990                            call.arguments.push(ast::LabeledArg {
5991                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5992                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5993                                    value: ast::LiteralValue::Bool(true),
5994                                    raw: "true".to_string(),
5995                                    digest: None,
5996                                }))),
5997                            });
5998                        }
5999                    } else {
6000                        // Remove construction kwarg if it exists
6001                        call.arguments
6002                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6003                    }
6004                }
6005                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6006            }
6007        }
6008        AstMutateCommand::EditControlPointSpline { points, construction } => {
6009            if let NodeMut::CallExpressionKw(call) = node {
6010                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6011                    return TraversalReturn::new_continue(());
6012                }
6013                for labeled_arg in &mut call.arguments {
6014                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6015                    {
6016                        labeled_arg.arg = points.clone();
6017                    }
6018                }
6019                // Handle construction kwarg
6020                if let Some(construction_value) = construction {
6021                    let construction_exists = call
6022                        .arguments
6023                        .iter()
6024                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6025                    if *construction_value {
6026                        if construction_exists {
6027                            for labeled_arg in &mut call.arguments {
6028                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6029                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6030                                        value: ast::LiteralValue::Bool(true),
6031                                        raw: "true".to_string(),
6032                                        digest: None,
6033                                    })));
6034                                }
6035                            }
6036                        } else {
6037                            call.arguments.push(ast::LabeledArg {
6038                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6039                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6040                                    value: ast::LiteralValue::Bool(true),
6041                                    raw: "true".to_string(),
6042                                    digest: None,
6043                                }))),
6044                            });
6045                        }
6046                    } else {
6047                        call.arguments
6048                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6049                    }
6050                }
6051                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6052            }
6053        }
6054        AstMutateCommand::EditConstraintValue { value } => {
6055            if let NodeMut::BinaryExpression(binary_expr) = node {
6056                let left_is_constraint = matches!(
6057                    &binary_expr.left,
6058                    ast::BinaryPart::CallExpressionKw(call)
6059                        if matches!(
6060                            call.callee.name.name.as_str(),
6061                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6062                        )
6063                );
6064                if left_is_constraint {
6065                    binary_expr.right = value.clone();
6066                } else {
6067                    binary_expr.left = value.clone();
6068                }
6069
6070                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6071            }
6072        }
6073        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6074            if let NodeMut::BinaryExpression(binary_expr) = node {
6075                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6076                    return TraversalReturn::new_continue(());
6077                };
6078                if !matches!(
6079                    call.callee.name.name.as_str(),
6080                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6081                ) {
6082                    return TraversalReturn::new_continue(());
6083                }
6084
6085                if let Some(label_arg) = call
6086                    .arguments
6087                    .iter_mut()
6088                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6089                {
6090                    label_arg.arg = label_position.clone();
6091                } else {
6092                    call.arguments.push(ast::LabeledArg {
6093                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6094                        arg: label_position.clone(),
6095                    });
6096                }
6097
6098                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6099            }
6100        }
6101        AstMutateCommand::EditCallUnlabeled { arg } => {
6102            if let NodeMut::CallExpressionKw(call) = node {
6103                call.unlabeled = Some(arg.clone());
6104                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6105            }
6106        }
6107        AstMutateCommand::EditVarInitialValue { value } => {
6108            // We target the SketchVar itself (matched by NodePath) rather than
6109            // the inner NumericLiteral so we can also write back into vars that
6110            // were declared without an initial value (e.g. bare `var`).
6111            if let NodeMut::SketchVar(sketch_var) = node {
6112                let Ok(literal) = to_source_number(*value) else {
6113                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6114                        "Could not convert number to AST literal: {:?}",
6115                        *value
6116                    ))));
6117                };
6118                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6119                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120            }
6121        }
6122        AstMutateCommand::DeleteNode => {
6123            return TraversalReturn {
6124                mutate_body_item: MutateBodyItem::Delete,
6125                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6126            };
6127        }
6128    }
6129    TraversalReturn::new_continue(())
6130}
6131
6132struct FindSketchBlockSourceRange {
6133    /// The source range of the sketch block before mutation.
6134    target_before_mutation: SourceRange,
6135    /// The source range of the sketch block's last body item after mutation. We
6136    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6137    /// shared reference.
6138    found: Cell<Option<AstNodeRef>>,
6139}
6140
6141impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6142    type Error = crate::front::Error;
6143
6144    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6145        let Ok(node_range) = SourceRange::try_from(&node) else {
6146            return Ok(true);
6147        };
6148
6149        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6150            if node_range.module_id() == self.target_before_mutation.module_id()
6151                && node_range.start() == self.target_before_mutation.start()
6152                // End shouldn't match since we added something.
6153                && node_range.end() >= self.target_before_mutation.end()
6154            {
6155                self.found.set(sketch_block.body.items.last().map(|item| match item {
6156                    // For declarations like `circle1 = circle(...)`, use
6157                    // the init expression range so lookup in source_range_to_object
6158                    // matches the segment source range.
6159                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6160                    _ => AstNodeRef::from(item),
6161                }));
6162                return Ok(false);
6163            } else {
6164                // We found a different sketch block. No need to descend into
6165                // its children since sketch blocks cannot be nested.
6166                return Ok(true);
6167            }
6168        }
6169
6170        for child in node.children().iter() {
6171            if !child.visit(*self)? {
6172                return Ok(false);
6173            }
6174        }
6175
6176        Ok(true)
6177    }
6178}
6179
6180struct FindSketchBlockByNodePath {
6181    /// The Node Path of the sketch block before mutation.
6182    target_node_path: ast::NodePath,
6183    /// The ref of the sketch block's last body item after mutation. We need to
6184    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6185    /// reference.
6186    found: Cell<Option<AstNodeRef>>,
6187}
6188
6189impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6190    type Error = crate::front::Error;
6191
6192    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6193        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6194            return Ok(true);
6195        };
6196
6197        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6198            if let Some(node_path) = node_path
6199                && node_path == self.target_node_path
6200            {
6201                self.found.set(sketch_block.body.items.last().map(|item| match item {
6202                    // For declarations like `circle1 = circle(...)`, use
6203                    // the init expression range so lookup in source_range_to_object
6204                    // matches the segment source range.
6205                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6206                    _ => AstNodeRef::from(item),
6207                }));
6208
6209                return Ok(false);
6210            } else {
6211                // We found a different sketch block. No need to descend into
6212                // its children since sketch blocks cannot be nested.
6213                return Ok(true);
6214            }
6215        }
6216
6217        for child in node.children().iter() {
6218            if !child.visit(*self)? {
6219                return Ok(false);
6220            }
6221        }
6222
6223        Ok(true)
6224    }
6225}
6226
6227/// After adding an item to a sketch block, find the sketch block, and get the
6228/// source range of the added item. We assume that the added item is the last
6229/// item in the sketch block and that the sketch block's source range has grown,
6230/// but not moved from its starting offset.
6231///
6232/// TODO: Do we need to format *before* mutation in case formatting moves the
6233/// sketch block forward?
6234fn find_sketch_block_added_item(
6235    ast: &ast::Node<ast::Program>,
6236    sketch_block_before_mutation: &AstNodeRef,
6237) -> Result<AstNodeRef, KclError> {
6238    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6239        let find = FindSketchBlockByNodePath {
6240            target_node_path: node_path.clone(),
6241            found: Cell::new(None),
6242        };
6243        let node = crate::walk::Node::from(ast);
6244        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6245        find.found.into_inner().ok_or_else(|| {
6246            KclError::refactor(format!(
6247                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6248            ))
6249        })
6250    } else {
6251        // No NodePath. Fall back to legacy source range.
6252        let find = FindSketchBlockSourceRange {
6253            target_before_mutation: sketch_block_before_mutation.range,
6254            found: Cell::new(None),
6255        };
6256        let node = crate::walk::Node::from(ast);
6257        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6258        find.found.into_inner().ok_or_else(|| KclError::refactor(
6259            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?"),
6260        ))
6261    }
6262}
6263
6264fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6265    // TODO: Don't duplicate this from lib.rs Program.
6266    ast.recast_top(&Default::default(), 0)
6267}
6268
6269struct FindNumericLiteral {
6270    target: SourceRange,
6271    found: Cell<Option<ast::NumericLiteral>>,
6272}
6273
6274impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6275    type Error = crate::front::Error;
6276
6277    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6278        let Ok(node_range) = SourceRange::try_from(&node) else {
6279            return Ok(true);
6280        };
6281
6282        if node_range == self.target
6283            && let crate::walk::Node::NumericLiteral(literal) = node
6284        {
6285            self.found.set(Some(literal.inner.clone()));
6286            return Ok(false);
6287        }
6288
6289        for child in node.children().iter() {
6290            if !child.visit(*self)? {
6291                return Ok(false);
6292            }
6293        }
6294
6295        Ok(true)
6296    }
6297}
6298
6299fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6300    let find = FindNumericLiteral {
6301        target,
6302        found: Cell::new(None),
6303    };
6304    let node = crate::walk::Node::from(ast);
6305    node.visit(&find).ok()?;
6306    find.found.into_inner()
6307}
6308
6309struct FindSketchVarInitialByNodePath<'a> {
6310    target: &'a ast::NodePath,
6311    sketch_var_found: Cell<bool>,
6312    initial_literal: Cell<Option<ast::NumericLiteral>>,
6313}
6314
6315impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6316    type Error = crate::front::Error;
6317
6318    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6319        if let crate::walk::Node::SketchVar(sketch_var) = node
6320            && sketch_var.node_path.as_ref() == Some(self.target)
6321        {
6322            self.sketch_var_found.set(true);
6323            if let Some(initial) = &sketch_var.initial {
6324                self.initial_literal.set(Some(initial.inner.clone()));
6325            }
6326            return Ok(false);
6327        }
6328
6329        for child in node.children().iter() {
6330            if !child.visit(*self)? {
6331                return Ok(false);
6332            }
6333        }
6334
6335        Ok(true)
6336    }
6337}
6338
6339/// Locate the source `var` declaration corresponding to a sketch-var solution.
6340///
6341/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6342/// from "target found." The inner [`Option`] is the initial numeric literal of
6343/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6344///
6345/// When `node_path` is `None` (e.g. for older outcomes that predate the
6346/// node-path propagation), this falls back to source-range matching, which
6347/// can break under whitespace shifts elsewhere in the file.
6348fn numeric_literal_at_node_path(
6349    ast: &ast::Node<ast::Program>,
6350    node_path: Option<&ast::NodePath>,
6351    source_range: SourceRange,
6352) -> Option<Option<ast::NumericLiteral>> {
6353    let Some(node_path) = node_path else {
6354        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";
6355        #[cfg(target_arch = "wasm32")]
6356        web_sys::console::warn_1(&message.into());
6357        #[cfg(not(target_arch = "wasm32"))]
6358        eprintln!("WARNING: {message}");
6359        return numeric_literal_at_source_range(ast, source_range).map(Some);
6360    };
6361    let find = FindSketchVarInitialByNodePath {
6362        target: node_path,
6363        sketch_var_found: Cell::new(false),
6364        initial_literal: Cell::new(None),
6365    };
6366    let node = crate::walk::Node::from(ast);
6367    node.visit(&find).ok()?;
6368    if !find.sketch_var_found.get() {
6369        return None;
6370    }
6371    Some(find.initial_literal.into_inner())
6372}
6373
6374fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6375    match suffix {
6376        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6377        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6378        NumericSuffix::M => Some(UnitLength::Meters),
6379        NumericSuffix::Inch => Some(UnitLength::Inches),
6380        NumericSuffix::Ft => Some(UnitLength::Feet),
6381        NumericSuffix::Yd => Some(UnitLength::Yards),
6382        _ => None,
6383    }
6384}
6385
6386fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6387    match suffix_length_unit(number.units) {
6388        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6389        None => number.value,
6390    }
6391}
6392
6393fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6394    match suffix_length_unit(literal.suffix) {
6395        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6396        None => literal.value,
6397    }
6398}
6399
6400fn var_solution_needs_commit(
6401    current_literal: &ast::NumericLiteral,
6402    solved_value: Number,
6403    default_length_unit: UnitLength,
6404) -> bool {
6405    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6406    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6407
6408    (current - solved).abs() > 1e-9
6409}
6410
6411fn preserve_var_solution_literal_style(
6412    current_literal: &ast::NumericLiteral,
6413    solved_value: Number,
6414    default_length_unit: UnitLength,
6415) -> Number {
6416    if current_literal.suffix == NumericSuffix::None {
6417        return Number {
6418            value: number_value_in_default_length_units(solved_value, default_length_unit),
6419            units: NumericSuffix::None,
6420        };
6421    }
6422
6423    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6424        return solved_value;
6425    };
6426
6427    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6428    Number {
6429        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6430        units: current_literal.suffix,
6431    }
6432}
6433
6434pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6435    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6436        inner: ast::ArrayExpression {
6437            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6438            non_code_meta: Default::default(),
6439            digest: None,
6440        },
6441        start: Default::default(),
6442        end: Default::default(),
6443        module_id: Default::default(),
6444        node_path: None,
6445        outer_attrs: Default::default(),
6446        pre_comments: Default::default(),
6447        comment_start: Default::default(),
6448    })))
6449}
6450
6451pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6452    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6453        ast::ArrayExpression {
6454            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6455            digest: None,
6456            non_code_meta: Default::default(),
6457        },
6458    ))))
6459}
6460
6461fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6462    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6463        ast::ArrayExpression {
6464            elements: vec![
6465                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6466                    point.x,
6467                )?)))),
6468                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6469                    point.y,
6470                )?)))),
6471            ],
6472            non_code_meta: Default::default(),
6473            digest: None,
6474        },
6475    ))))
6476}
6477
6478fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6479    match expr {
6480        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6481            inner: ast::Literal::from(to_source_number(*number)?),
6482            start: Default::default(),
6483            end: Default::default(),
6484            module_id: Default::default(),
6485            node_path: None,
6486            outer_attrs: Default::default(),
6487            pre_comments: Default::default(),
6488            comment_start: Default::default(),
6489        }))),
6490        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6491            inner: ast::SketchVar {
6492                initial: Some(Box::new(ast::Node {
6493                    inner: to_source_number(*number)?,
6494                    start: Default::default(),
6495                    end: Default::default(),
6496                    module_id: Default::default(),
6497                    node_path: None,
6498                    outer_attrs: Default::default(),
6499                    pre_comments: Default::default(),
6500                    comment_start: Default::default(),
6501                })),
6502                digest: None,
6503            },
6504            start: Default::default(),
6505            end: Default::default(),
6506            module_id: Default::default(),
6507            node_path: None,
6508            outer_attrs: Default::default(),
6509            pre_comments: Default::default(),
6510            comment_start: Default::default(),
6511        }))),
6512        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6513    }
6514}
6515
6516fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6517    Ok(ast::NumericLiteral {
6518        value: number.value,
6519        suffix: number.units,
6520        raw: format_number_literal(number.value, number.units, None)?,
6521        digest: None,
6522    })
6523}
6524
6525pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6526    ast::Expr::Name(Box::new(ast_name(name)))
6527}
6528
6529fn ast_name(name: String) -> ast::Node<ast::Name> {
6530    ast::Node {
6531        inner: ast::Name {
6532            name: ast::Node {
6533                inner: ast::Identifier { name, digest: None },
6534                start: Default::default(),
6535                end: Default::default(),
6536                module_id: Default::default(),
6537                node_path: None,
6538                outer_attrs: Default::default(),
6539                pre_comments: Default::default(),
6540                comment_start: Default::default(),
6541            },
6542            path: Vec::new(),
6543            abs_path: false,
6544            digest: None,
6545        },
6546        start: Default::default(),
6547        end: Default::default(),
6548        module_id: Default::default(),
6549        node_path: None,
6550        outer_attrs: Default::default(),
6551        pre_comments: Default::default(),
6552        comment_start: Default::default(),
6553    }
6554}
6555
6556pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6557    ast::Name {
6558        name: ast::Node {
6559            inner: ast::Identifier {
6560                name: name.to_owned(),
6561                digest: None,
6562            },
6563            start: Default::default(),
6564            end: Default::default(),
6565            module_id: Default::default(),
6566            node_path: None,
6567            outer_attrs: Default::default(),
6568            pre_comments: Default::default(),
6569            comment_start: Default::default(),
6570        },
6571        path: Default::default(),
6572        abs_path: false,
6573        digest: None,
6574    }
6575}
6576
6577// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6578
6579/// Create an AST node for coincident([expr1, expr2, ...])
6580pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6581    let elements = exprs.into_iter().collect::<Vec<_>>();
6582    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6583
6584    // Create array [expr1, expr2, ...]
6585    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6586        elements,
6587        digest: None,
6588        non_code_meta: Default::default(),
6589    })));
6590
6591    // Create coincident([...])
6592    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6593        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6594        unlabeled: Some(array_expr),
6595        arguments: Default::default(),
6596        digest: None,
6597        non_code_meta: Default::default(),
6598    })))
6599}
6600
6601/// Create an AST node for line(start = [...], end = [...])
6602pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6603    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6604        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6605        unlabeled: None,
6606        arguments: vec![
6607            ast::LabeledArg {
6608                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6609                arg: start_ast,
6610            },
6611            ast::LabeledArg {
6612                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6613                arg: end_ast,
6614            },
6615        ],
6616        digest: None,
6617        non_code_meta: Default::default(),
6618    })))
6619}
6620
6621/// Create an AST node for arc(start = [...], end = [...], center = [...])
6622pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6623    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6624        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6625        unlabeled: None,
6626        arguments: vec![
6627            ast::LabeledArg {
6628                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6629                arg: start_ast,
6630            },
6631            ast::LabeledArg {
6632                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6633                arg: end_ast,
6634            },
6635            ast::LabeledArg {
6636                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6637                arg: center_ast,
6638            },
6639        ],
6640        digest: None,
6641        non_code_meta: Default::default(),
6642    })))
6643}
6644
6645/// Create an AST node for circle(start = [...], center = [...])
6646pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6647    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6648        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6649        unlabeled: None,
6650        arguments: vec![
6651            ast::LabeledArg {
6652                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6653                arg: start_ast,
6654            },
6655            ast::LabeledArg {
6656                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6657                arg: center_ast,
6658            },
6659        ],
6660        digest: None,
6661        non_code_meta: Default::default(),
6662    })))
6663}
6664
6665/// Create an AST node for horizontal(line)
6666pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6667    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6668        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6669        unlabeled: Some(line_expr),
6670        arguments: Default::default(),
6671        digest: None,
6672        non_code_meta: Default::default(),
6673    })))
6674}
6675
6676/// Create an AST node for vertical(line)
6677pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6678    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6679        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6680        unlabeled: Some(line_expr),
6681        arguments: Default::default(),
6682        digest: None,
6683        non_code_meta: Default::default(),
6684    })))
6685}
6686
6687/// Create a member expression like object.property (e.g., line1.end)
6688pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6689    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6690        object: object_expr,
6691        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6692            name: ast::Node::no_src(ast::Identifier {
6693                name: property.to_string(),
6694                digest: None,
6695            }),
6696            path: Vec::new(),
6697            abs_path: false,
6698            digest: None,
6699        }))),
6700        computed: false,
6701        digest: None,
6702    })))
6703}
6704
6705pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6706    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6707        object: object_expr,
6708        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6709            value: index as f64,
6710            suffix: NumericSuffix::None,
6711            raw: index.to_string(),
6712            digest: None,
6713        })))),
6714        computed: true,
6715        digest: None,
6716    })))
6717}
6718
6719/// Create an AST node for `fixed([point, [x, y]])`.
6720fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6721    // Create [x, y] array literal.
6722    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6723        position.x,
6724    )?))));
6725    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6726        position.y,
6727    )?))));
6728    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6729        elements: vec![x_literal, y_literal],
6730        digest: None,
6731        non_code_meta: Default::default(),
6732    })));
6733
6734    // Create [point, [x, y]] outer array.
6735    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6736        elements: vec![point_expr, point_array],
6737        digest: None,
6738        non_code_meta: Default::default(),
6739    })));
6740
6741    // Create fixed([...])
6742    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6743        ast::CallExpressionKw {
6744            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6745            unlabeled: Some(array_expr),
6746            arguments: Default::default(),
6747            digest: None,
6748            non_code_meta: Default::default(),
6749        },
6750    ))))
6751}
6752
6753/// Create an AST node for equalLength([line1, line2, ...])
6754pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6755    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6756        elements: line_exprs,
6757        digest: None,
6758        non_code_meta: Default::default(),
6759    })));
6760
6761    // Create equalLength([...])
6762    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6763        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6764        unlabeled: Some(array_expr),
6765        arguments: Default::default(),
6766        digest: None,
6767        non_code_meta: Default::default(),
6768    })))
6769}
6770
6771/// Create an AST node for equalRadius([seg1, seg2, ...])
6772pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6773    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6774        elements: segment_exprs,
6775        digest: None,
6776        non_code_meta: Default::default(),
6777    })));
6778
6779    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6780        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6781        unlabeled: Some(array_expr),
6782        arguments: Default::default(),
6783        digest: None,
6784        non_code_meta: Default::default(),
6785    })))
6786}
6787
6788/// Create an AST node for tangent([seg1, seg2])
6789pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6790    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6791        elements: vec![seg1_expr, seg2_expr],
6792        digest: None,
6793        non_code_meta: Default::default(),
6794    })));
6795
6796    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6797        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6798        unlabeled: Some(array_expr),
6799        arguments: Default::default(),
6800        digest: None,
6801        non_code_meta: Default::default(),
6802    })))
6803}
6804
6805/// Create an AST node for symmetric([input1, input2], axis = line)
6806pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6807    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6808        elements: input_exprs,
6809        digest: None,
6810        non_code_meta: Default::default(),
6811    })));
6812    let arguments = vec![ast::LabeledArg {
6813        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6814        arg: axis_expr,
6815    }];
6816
6817    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6818        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6819        unlabeled: Some(array_expr),
6820        arguments,
6821        digest: None,
6822        non_code_meta: Default::default(),
6823    })))
6824}
6825
6826/// Create an AST node for midpoint(segment, point = point)
6827pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6828    let arguments = vec![ast::LabeledArg {
6829        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6830        arg: point_expr,
6831    }];
6832
6833    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6834        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6835        unlabeled: Some(segment_expr),
6836        arguments,
6837        digest: None,
6838        non_code_meta: Default::default(),
6839    })))
6840}
6841
6842#[cfg(test)]
6843mod tests {
6844    use super::*;
6845    use crate::engine::PlaneName;
6846    use crate::execution::cache::SketchModeState;
6847    use crate::execution::cache::clear_mem_cache;
6848    use crate::execution::cache::read_old_memory;
6849    use crate::execution::cache::write_old_memory;
6850    use crate::front::Distance;
6851    use crate::front::Fixed;
6852    use crate::front::FixedPoint;
6853    use crate::front::Midpoint;
6854    use crate::front::Object;
6855    use crate::front::Plane;
6856    use crate::front::Sketch;
6857    use crate::front::Tangent;
6858    use crate::frontend::sketch::Vertical;
6859    use crate::pretty::NumericSuffix;
6860
6861    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6862        for object in &scene_graph.objects {
6863            if let ObjectKind::Sketch(_) = &object.kind {
6864                return Some(object);
6865            }
6866        }
6867        None
6868    }
6869
6870    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6871        for object in &scene_graph.objects {
6872            if let ObjectKind::Face(_) = &object.kind {
6873                return Some(object);
6874            }
6875        }
6876        None
6877    }
6878
6879    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6880        for object in &scene_graph.objects {
6881            if matches!(&object.kind, ObjectKind::Wall(_)) {
6882                return Some(object.id);
6883            }
6884        }
6885        None
6886    }
6887
6888    #[test]
6889    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6890        let source = "\
6891region001 = region(point = [0.1, 0.1], sketch = s)
6892extrude001 = extrude(region001, length = 5)
6893revolve001 = revolve(region001, axis = Y)
6894sweep001 = sweep(region001, path = path001)
6895loft001 = loft(region001)
6896not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6897";
6898
6899        let program = Program::parse(source).unwrap().0.unwrap();
6900
6901        assert_eq!(
6902            region_name_from_sweep_variable(&program.ast, "extrude001"),
6903            Some("region001".to_owned())
6904        );
6905        assert_eq!(
6906            region_name_from_sweep_variable(&program.ast, "revolve001"),
6907            Some("region001".to_owned())
6908        );
6909        assert_eq!(
6910            region_name_from_sweep_variable(&program.ast, "sweep001"),
6911            Some("region001".to_owned())
6912        );
6913        assert_eq!(
6914            region_name_from_sweep_variable(&program.ast, "loft001"),
6915            Some("region001".to_owned())
6916        );
6917        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6918    }
6919
6920    #[track_caller]
6921    fn expect_sketch(object: &Object) -> &Sketch {
6922        if let ObjectKind::Sketch(sketch) = &object.kind {
6923            sketch
6924        } else {
6925            panic!("Object is not a sketch: {:?}", object);
6926        }
6927    }
6928
6929    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6930        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6931        let ObjectKind::Segment {
6932            segment: Segment::Point(point),
6933        } = &point_object.kind
6934        else {
6935            panic!("Object is not a point segment: {point_object:?}");
6936        };
6937        point.position.clone()
6938    }
6939
6940    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6941        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6942        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6943    }
6944
6945    /// Build a millimeter-valued point expression for concise sketch edit test
6946    /// setup.
6947    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
6948        Point2d {
6949            x: Expr::Var(Number {
6950                value: x,
6951                units: NumericSuffix::Mm,
6952            }),
6953            y: Expr::Var(Number {
6954                value: y,
6955                units: NumericSuffix::Mm,
6956            }),
6957        }
6958    }
6959
6960    /// Build a millimeter-valued numeric point for comparing solved scene graph
6961    /// positions.
6962    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
6963        Point2d {
6964            x: Number {
6965                value: x,
6966                units: NumericSuffix::Mm,
6967            },
6968            y: Number {
6969                value: y,
6970                units: NumericSuffix::Mm,
6971            },
6972        }
6973    }
6974
6975    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6976        LineCtor {
6977            start: Point2d {
6978                x: Expr::Number(Number { value: start_x, units }),
6979                y: Expr::Number(Number { value: start_y, units }),
6980            },
6981            end: Point2d {
6982                x: Expr::Number(Number { value: end_x, units }),
6983                y: Expr::Number(Number { value: end_y, units }),
6984            },
6985            construction: None,
6986        }
6987    }
6988
6989    async fn create_sketch_with_single_line(
6990        frontend: &mut FrontendState,
6991        ctx: &ExecutorContext,
6992        mock_ctx: &ExecutorContext,
6993        version: Version,
6994    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6995        frontend.program = Program::empty();
6996
6997        let sketch_args = SketchCtor {
6998            on: Plane::Default(PlaneName::Xy),
6999        };
7000        let (_src_delta, _scene_delta, sketch_id) = frontend
7001            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7002            .await
7003            .unwrap();
7004
7005        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7006        let (source_delta, scene_graph_delta) = frontend
7007            .add_segment(mock_ctx, version, sketch_id, segment, None)
7008            .await
7009            .unwrap();
7010        let line_id = *scene_graph_delta
7011            .new_objects
7012            .last()
7013            .expect("Expected line object id to be created");
7014
7015        (sketch_id, line_id, source_delta, scene_graph_delta)
7016    }
7017
7018    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7019        frontend.program = program.clone();
7020        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7021        frontend.update_state_after_exec(outcome, true);
7022    }
7023
7024    #[tokio::test(flavor = "multi_thread")]
7025    async fn test_sketch_checkpoint_round_trip_restores_state() {
7026        let mut frontend = FrontendState::new();
7027        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7028        let mock_ctx = ExecutorContext::new_mock(None).await;
7029        let version = Version(0);
7030
7031        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7032            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7033
7034        let expected_source = source_delta.text.clone();
7035        let expected_scene_graph = frontend.scene_graph.clone();
7036        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7037        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7038
7039        let checkpoint_id = frontend
7040            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7041            .await
7042            .unwrap();
7043
7044        let edited_segments = vec![ExistingSegmentCtor {
7045            id: line_id,
7046            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7047        }];
7048        let (edited_source, _edited_scene) = frontend
7049            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7050            .await
7051            .unwrap();
7052        assert_ne!(edited_source.text, expected_source);
7053
7054        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7055
7056        assert_eq!(restored.source_delta.text, expected_source);
7057        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7058        assert!(restored.scene_graph_delta.invalidates_ids);
7059        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7060        assert_eq!(frontend.scene_graph, expected_scene_graph);
7061        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7062
7063        ctx.close().await;
7064    }
7065
7066    #[tokio::test(flavor = "multi_thread")]
7067    async fn test_sketch_checkpoints_prune_oldest_entries() {
7068        let mut frontend = FrontendState::new();
7069        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7070        let mock_ctx = ExecutorContext::new_mock(None).await;
7071        let version = Version(0);
7072
7073        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7074            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7075
7076        let mut checkpoint_ids = Vec::new();
7077        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7078            checkpoint_ids.push(
7079                frontend
7080                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7081                    .await
7082                    .unwrap(),
7083            );
7084        }
7085
7086        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7087        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7088
7089        let oldest_retained = checkpoint_ids[3];
7090        assert_eq!(
7091            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7092            Some(oldest_retained)
7093        );
7094
7095        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7096        assert!(evicted_restore.is_err());
7097        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7098
7099        frontend
7100            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7101            .await
7102            .unwrap();
7103
7104        ctx.close().await;
7105    }
7106
7107    #[tokio::test(flavor = "multi_thread")]
7108    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7109        let mut frontend = FrontendState::new();
7110        let missing_checkpoint = SketchCheckpointId::new(999);
7111
7112        let err = frontend
7113            .restore_sketch_checkpoint(missing_checkpoint)
7114            .await
7115            .expect_err("Expected restore to fail for missing checkpoint");
7116
7117        assert!(err.msg.contains("Sketch checkpoint not found"));
7118    }
7119
7120    #[tokio::test(flavor = "multi_thread")]
7121    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7122        let mut frontend = FrontendState::new();
7123        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7124        let mock_ctx = ExecutorContext::new_mock(None).await;
7125        let version = Version(0);
7126
7127        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7128            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7129
7130        let checkpoint_a = frontend
7131            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7132            .await
7133            .unwrap();
7134        let checkpoint_b = frontend
7135            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7136            .await
7137            .unwrap();
7138        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7139
7140        frontend.clear_sketch_checkpoints();
7141        assert!(frontend.sketch_checkpoints.is_empty());
7142        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7143        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7144
7145        ctx.close().await;
7146    }
7147
7148    #[tokio::test(flavor = "multi_thread")]
7149    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7150        let mut frontend = FrontendState::new();
7151        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7152        let mock_ctx = ExecutorContext::new_mock(None).await;
7153        let version = Version(0);
7154
7155        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7156            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7157        let old_source = source_delta.text.clone();
7158        let old_checkpoint = frontend
7159            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7160            .await
7161            .unwrap();
7162        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7163
7164        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7165            .unwrap()
7166            .0
7167            .unwrap();
7168
7169        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7170        let SetProgramOutcome::Success {
7171            checkpoint_id: Some(new_checkpoint),
7172            ..
7173        } = result
7174        else {
7175            panic!("Expected Success with a fresh checkpoint baseline");
7176        };
7177
7178        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7179
7180        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7181        assert_eq!(old_restore.source_delta.text, old_source);
7182
7183        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7184        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7185
7186        ctx.close().await;
7187    }
7188
7189    #[tokio::test(flavor = "multi_thread")]
7190    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7191        let mut frontend = FrontendState::new();
7192        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7193        let mock_ctx = ExecutorContext::new_mock(None).await;
7194        let version = Version(0);
7195
7196        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7197            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7198        let old_checkpoint = frontend
7199            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7200            .await
7201            .unwrap();
7202        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7203
7204        let failing_program = Program::parse(
7205            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7206        )
7207        .unwrap()
7208        .0
7209        .unwrap();
7210
7211        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7212        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7213        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7214        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7215
7216        ctx.close().await;
7217    }
7218
7219    #[tokio::test(flavor = "multi_thread")]
7220    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7221        let mut frontend = FrontendState::new();
7222        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7223
7224        let program = Program::parse(
7225            "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",
7226        )
7227        .unwrap()
7228        .0
7229        .unwrap();
7230        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7231        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7232            panic!("Expected successful baseline program execution");
7233        };
7234
7235        clear_mem_cache().await;
7236        assert!(read_old_memory().await.is_none());
7237
7238        let checkpoint_without_mock_memory = frontend
7239            .create_sketch_checkpoint((*exec_outcome).clone())
7240            .await
7241            .unwrap();
7242
7243        write_old_memory(SketchModeState::new_for_tests()).await;
7244        assert!(read_old_memory().await.is_some());
7245
7246        let checkpoint_with_mock_memory = frontend
7247            .create_sketch_checkpoint((*exec_outcome).clone())
7248            .await
7249            .unwrap();
7250
7251        clear_mem_cache().await;
7252        assert!(read_old_memory().await.is_none());
7253
7254        frontend
7255            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7256            .await
7257            .unwrap();
7258        assert!(read_old_memory().await.is_some());
7259
7260        frontend
7261            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7262            .await
7263            .unwrap();
7264        assert!(read_old_memory().await.is_none());
7265
7266        ctx.close().await;
7267    }
7268
7269    #[tokio::test(flavor = "multi_thread")]
7270    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7271        let source = "\
7272sketch(on = XY) {
7273  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7274}
7275
7276bad = missing_name
7277";
7278        let program = Program::parse(source).unwrap().0.unwrap();
7279
7280        let mut frontend = FrontendState::new();
7281
7282        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7283        let mock_ctx = ExecutorContext::new_mock(None).await;
7284        let version = Version(0);
7285        let project_id = ProjectId(0);
7286        let file_id = FileId(0);
7287
7288        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7289            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7290        };
7291
7292        let sketch_id = frontend
7293            .scene_graph
7294            .objects
7295            .iter()
7296            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7297            .expect("Expected sketch object from errored hack_set_program");
7298
7299        frontend
7300            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7301            .await
7302            .unwrap();
7303
7304        ctx.close().await;
7305        mock_ctx.close().await;
7306    }
7307
7308    #[tokio::test(flavor = "multi_thread")]
7309    async fn test_new_sketch_add_point_edit_point() {
7310        let program = Program::empty();
7311
7312        let mut frontend = FrontendState::new();
7313        frontend.program = program;
7314
7315        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7316        let mock_ctx = ExecutorContext::new_mock(None).await;
7317        let version = Version(0);
7318
7319        let sketch_args = SketchCtor {
7320            on: Plane::Default(PlaneName::Xy),
7321        };
7322        let (_src_delta, scene_delta, sketch_id) = frontend
7323            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7324            .await
7325            .unwrap();
7326        assert_eq!(sketch_id, ObjectId(1));
7327        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7328        let sketch_object = &scene_delta.new_graph.objects[1];
7329        assert_eq!(sketch_object.id, ObjectId(1));
7330        assert_eq!(
7331            sketch_object.kind,
7332            ObjectKind::Sketch(Sketch {
7333                args: SketchCtor {
7334                    on: Plane::Default(PlaneName::Xy)
7335                },
7336                plane: ObjectId(0),
7337                segments: vec![],
7338                constraints: vec![],
7339            })
7340        );
7341        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7342
7343        let point_ctor = PointCtor {
7344            position: Point2d {
7345                x: Expr::Number(Number {
7346                    value: 1.0,
7347                    units: NumericSuffix::Inch,
7348                }),
7349                y: Expr::Number(Number {
7350                    value: 2.0,
7351                    units: NumericSuffix::Inch,
7352                }),
7353            },
7354        };
7355        let segment = SegmentCtor::Point(point_ctor);
7356        let (src_delta, scene_delta) = frontend
7357            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7358            .await
7359            .unwrap();
7360        assert_eq!(
7361            src_delta.text.as_str(),
7362            "sketch001 = sketch(on = XY) {
7363  point(at = [1in, 2in])
7364}
7365"
7366        );
7367        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7368        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7369        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7370            assert_eq!(scene_object.id.0, i);
7371        }
7372
7373        let point_id = *scene_delta.new_objects.last().unwrap();
7374
7375        let point_ctor = PointCtor {
7376            position: Point2d {
7377                x: Expr::Number(Number {
7378                    value: 3.0,
7379                    units: NumericSuffix::Inch,
7380                }),
7381                y: Expr::Number(Number {
7382                    value: 4.0,
7383                    units: NumericSuffix::Inch,
7384                }),
7385            },
7386        };
7387        let segments = vec![ExistingSegmentCtor {
7388            id: point_id,
7389            ctor: SegmentCtor::Point(point_ctor),
7390        }];
7391        let (src_delta, scene_delta) = frontend
7392            .edit_segments(&mock_ctx, version, sketch_id, segments)
7393            .await
7394            .unwrap();
7395        assert_eq!(
7396            src_delta.text.as_str(),
7397            "sketch001 = sketch(on = XY) {
7398  point(at = [3in, 4in])
7399}
7400"
7401        );
7402        assert_eq!(scene_delta.new_objects, vec![]);
7403        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7404
7405        ctx.close().await;
7406        mock_ctx.close().await;
7407    }
7408
7409    #[tokio::test(flavor = "multi_thread")]
7410    async fn test_new_sketch_add_line_edit_line() {
7411        let program = Program::empty();
7412
7413        let mut frontend = FrontendState::new();
7414        frontend.program = program;
7415
7416        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7417        let mock_ctx = ExecutorContext::new_mock(None).await;
7418        let version = Version(0);
7419
7420        let sketch_args = SketchCtor {
7421            on: Plane::Default(PlaneName::Xy),
7422        };
7423        let (_src_delta, scene_delta, sketch_id) = frontend
7424            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7425            .await
7426            .unwrap();
7427        assert_eq!(sketch_id, ObjectId(1));
7428        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7429        let sketch_object = &scene_delta.new_graph.objects[1];
7430        assert_eq!(sketch_object.id, ObjectId(1));
7431        assert_eq!(
7432            sketch_object.kind,
7433            ObjectKind::Sketch(Sketch {
7434                args: SketchCtor {
7435                    on: Plane::Default(PlaneName::Xy)
7436                },
7437                plane: ObjectId(0),
7438                segments: vec![],
7439                constraints: vec![],
7440            })
7441        );
7442        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7443
7444        let line_ctor = LineCtor {
7445            start: Point2d {
7446                x: Expr::Number(Number {
7447                    value: 0.0,
7448                    units: NumericSuffix::Mm,
7449                }),
7450                y: Expr::Number(Number {
7451                    value: 0.0,
7452                    units: NumericSuffix::Mm,
7453                }),
7454            },
7455            end: Point2d {
7456                x: Expr::Number(Number {
7457                    value: 10.0,
7458                    units: NumericSuffix::Mm,
7459                }),
7460                y: Expr::Number(Number {
7461                    value: 10.0,
7462                    units: NumericSuffix::Mm,
7463                }),
7464            },
7465            construction: None,
7466        };
7467        let segment = SegmentCtor::Line(line_ctor);
7468        let (src_delta, scene_delta) = frontend
7469            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7470            .await
7471            .unwrap();
7472        assert_eq!(
7473            src_delta.text.as_str(),
7474            "sketch001 = sketch(on = XY) {
7475  line(start = [0mm, 0mm], end = [10mm, 10mm])
7476}
7477"
7478        );
7479        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7480        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7481        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7482            assert_eq!(scene_object.id.0, i);
7483        }
7484
7485        // The new objects are the end points and then the line.
7486        let line = *scene_delta.new_objects.last().unwrap();
7487
7488        let line_ctor = LineCtor {
7489            start: Point2d {
7490                x: Expr::Number(Number {
7491                    value: 1.0,
7492                    units: NumericSuffix::Mm,
7493                }),
7494                y: Expr::Number(Number {
7495                    value: 2.0,
7496                    units: NumericSuffix::Mm,
7497                }),
7498            },
7499            end: Point2d {
7500                x: Expr::Number(Number {
7501                    value: 13.0,
7502                    units: NumericSuffix::Mm,
7503                }),
7504                y: Expr::Number(Number {
7505                    value: 14.0,
7506                    units: NumericSuffix::Mm,
7507                }),
7508            },
7509            construction: None,
7510        };
7511        let segments = vec![ExistingSegmentCtor {
7512            id: line,
7513            ctor: SegmentCtor::Line(line_ctor),
7514        }];
7515        let (src_delta, scene_delta) = frontend
7516            .edit_segments(&mock_ctx, version, sketch_id, segments)
7517            .await
7518            .unwrap();
7519        assert_eq!(
7520            src_delta.text.as_str(),
7521            "sketch001 = sketch(on = XY) {
7522  line(start = [1mm, 2mm], end = [13mm, 14mm])
7523}
7524"
7525        );
7526        assert_eq!(scene_delta.new_objects, vec![]);
7527        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7528
7529        ctx.close().await;
7530        mock_ctx.close().await;
7531    }
7532
7533    #[tokio::test(flavor = "multi_thread")]
7534    async fn test_new_sketch_add_arc_edit_arc() {
7535        let program = Program::empty();
7536
7537        let mut frontend = FrontendState::new();
7538        frontend.program = program;
7539
7540        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7541        let mock_ctx = ExecutorContext::new_mock(None).await;
7542        let version = Version(0);
7543
7544        let sketch_args = SketchCtor {
7545            on: Plane::Default(PlaneName::Xy),
7546        };
7547        let (_src_delta, scene_delta, sketch_id) = frontend
7548            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7549            .await
7550            .unwrap();
7551        assert_eq!(sketch_id, ObjectId(1));
7552        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7553        let sketch_object = &scene_delta.new_graph.objects[1];
7554        assert_eq!(sketch_object.id, ObjectId(1));
7555        assert_eq!(
7556            sketch_object.kind,
7557            ObjectKind::Sketch(Sketch {
7558                args: SketchCtor {
7559                    on: Plane::Default(PlaneName::Xy),
7560                },
7561                plane: ObjectId(0),
7562                segments: vec![],
7563                constraints: vec![],
7564            })
7565        );
7566        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7567
7568        let arc_ctor = ArcCtor {
7569            start: Point2d {
7570                x: Expr::Var(Number {
7571                    value: 0.0,
7572                    units: NumericSuffix::Mm,
7573                }),
7574                y: Expr::Var(Number {
7575                    value: 0.0,
7576                    units: NumericSuffix::Mm,
7577                }),
7578            },
7579            end: Point2d {
7580                x: Expr::Var(Number {
7581                    value: 10.0,
7582                    units: NumericSuffix::Mm,
7583                }),
7584                y: Expr::Var(Number {
7585                    value: 10.0,
7586                    units: NumericSuffix::Mm,
7587                }),
7588            },
7589            center: Point2d {
7590                x: Expr::Var(Number {
7591                    value: 10.0,
7592                    units: NumericSuffix::Mm,
7593                }),
7594                y: Expr::Var(Number {
7595                    value: 0.0,
7596                    units: NumericSuffix::Mm,
7597                }),
7598            },
7599            construction: None,
7600        };
7601        let segment = SegmentCtor::Arc(arc_ctor);
7602        let (src_delta, scene_delta) = frontend
7603            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7604            .await
7605            .unwrap();
7606        assert_eq!(
7607            src_delta.text.as_str(),
7608            "sketch001 = sketch(on = XY) {
7609  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7610}
7611"
7612        );
7613        assert_eq!(
7614            scene_delta.new_objects,
7615            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7616        );
7617        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7618            assert_eq!(scene_object.id.0, i);
7619        }
7620        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7621
7622        // The new objects are the end points, the center, and then the arc.
7623        let arc = *scene_delta.new_objects.last().unwrap();
7624
7625        let arc_ctor = ArcCtor {
7626            start: Point2d {
7627                x: Expr::Var(Number {
7628                    value: 1.0,
7629                    units: NumericSuffix::Mm,
7630                }),
7631                y: Expr::Var(Number {
7632                    value: 2.0,
7633                    units: NumericSuffix::Mm,
7634                }),
7635            },
7636            end: Point2d {
7637                x: Expr::Var(Number {
7638                    value: 13.0,
7639                    units: NumericSuffix::Mm,
7640                }),
7641                y: Expr::Var(Number {
7642                    value: 14.0,
7643                    units: NumericSuffix::Mm,
7644                }),
7645            },
7646            center: Point2d {
7647                x: Expr::Var(Number {
7648                    value: 13.0,
7649                    units: NumericSuffix::Mm,
7650                }),
7651                y: Expr::Var(Number {
7652                    value: 2.0,
7653                    units: NumericSuffix::Mm,
7654                }),
7655            },
7656            construction: None,
7657        };
7658        let segments = vec![ExistingSegmentCtor {
7659            id: arc,
7660            ctor: SegmentCtor::Arc(arc_ctor),
7661        }];
7662        let (src_delta, scene_delta) = frontend
7663            .edit_segments(&mock_ctx, version, sketch_id, segments)
7664            .await
7665            .unwrap();
7666        assert_eq!(
7667            src_delta.text.as_str(),
7668            "sketch001 = sketch(on = XY) {
7669  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7670}
7671"
7672        );
7673        assert_eq!(scene_delta.new_objects, vec![]);
7674        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7675
7676        ctx.close().await;
7677        mock_ctx.close().await;
7678    }
7679
7680    #[tokio::test(flavor = "multi_thread")]
7681    async fn test_new_sketch_add_circle_edit_circle() {
7682        let program = Program::empty();
7683
7684        let mut frontend = FrontendState::new();
7685        frontend.program = program;
7686
7687        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7688        let mock_ctx = ExecutorContext::new_mock(None).await;
7689        let version = Version(0);
7690
7691        let sketch_args = SketchCtor {
7692            on: Plane::Default(PlaneName::Xy),
7693        };
7694        let (_src_delta, _scene_delta, sketch_id) = frontend
7695            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7696            .await
7697            .unwrap();
7698
7699        // Add a circle segment.
7700        let circle_ctor = CircleCtor {
7701            start: Point2d {
7702                x: Expr::Var(Number {
7703                    value: 5.0,
7704                    units: NumericSuffix::Mm,
7705                }),
7706                y: Expr::Var(Number {
7707                    value: 0.0,
7708                    units: NumericSuffix::Mm,
7709                }),
7710            },
7711            center: Point2d {
7712                x: Expr::Var(Number {
7713                    value: 0.0,
7714                    units: NumericSuffix::Mm,
7715                }),
7716                y: Expr::Var(Number {
7717                    value: 0.0,
7718                    units: NumericSuffix::Mm,
7719                }),
7720            },
7721            construction: None,
7722        };
7723        let segment = SegmentCtor::Circle(circle_ctor);
7724        let (src_delta, scene_delta) = frontend
7725            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7726            .await
7727            .unwrap();
7728        assert_eq!(
7729            src_delta.text.as_str(),
7730            "sketch001 = sketch(on = XY) {
7731  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7732}
7733"
7734        );
7735        // The new objects are start, center, and then the circle segment.
7736        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7737        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7738
7739        let circle = *scene_delta.new_objects.last().unwrap();
7740
7741        // Edit the circle segment.
7742        let circle_ctor = CircleCtor {
7743            start: Point2d {
7744                x: Expr::Var(Number {
7745                    value: 10.0,
7746                    units: NumericSuffix::Mm,
7747                }),
7748                y: Expr::Var(Number {
7749                    value: 0.0,
7750                    units: NumericSuffix::Mm,
7751                }),
7752            },
7753            center: Point2d {
7754                x: Expr::Var(Number {
7755                    value: 3.0,
7756                    units: NumericSuffix::Mm,
7757                }),
7758                y: Expr::Var(Number {
7759                    value: 4.0,
7760                    units: NumericSuffix::Mm,
7761                }),
7762            },
7763            construction: None,
7764        };
7765        let segments = vec![ExistingSegmentCtor {
7766            id: circle,
7767            ctor: SegmentCtor::Circle(circle_ctor),
7768        }];
7769        let (src_delta, scene_delta) = frontend
7770            .edit_segments(&mock_ctx, version, sketch_id, segments)
7771            .await
7772            .unwrap();
7773        assert_eq!(
7774            src_delta.text.as_str(),
7775            "sketch001 = sketch(on = XY) {
7776  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7777}
7778"
7779        );
7780        assert_eq!(scene_delta.new_objects, vec![]);
7781        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7782
7783        ctx.close().await;
7784        mock_ctx.close().await;
7785    }
7786
7787    #[tokio::test(flavor = "multi_thread")]
7788    async fn test_delete_circle() {
7789        let initial_source = "sketch001 = sketch(on = XY) {
7790  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7791}
7792";
7793
7794        let program = Program::parse(initial_source).unwrap().0.unwrap();
7795        let mut frontend = FrontendState::new();
7796
7797        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7798        let mock_ctx = ExecutorContext::new_mock(None).await;
7799        let version = Version(0);
7800
7801        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7802        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7803        let sketch_id = sketch_object.id;
7804        let sketch = expect_sketch(sketch_object);
7805
7806        // The sketch should have 3 segments: start point, center point, and the circle.
7807        assert_eq!(sketch.segments.len(), 3);
7808        let circle_id = sketch.segments[2];
7809
7810        // Delete the circle.
7811        let (src_delta, scene_delta) = frontend
7812            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7813            .await
7814            .unwrap();
7815        assert_eq!(
7816            src_delta.text.as_str(),
7817            "sketch001 = sketch(on = XY) {
7818}
7819"
7820        );
7821        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7822        let new_sketch = expect_sketch(new_sketch_object);
7823        assert_eq!(new_sketch.segments.len(), 0);
7824
7825        ctx.close().await;
7826        mock_ctx.close().await;
7827    }
7828
7829    #[tokio::test(flavor = "multi_thread")]
7830    async fn test_edit_circle_via_point() {
7831        let initial_source = "sketch001 = sketch(on = XY) {
7832  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7833}
7834";
7835
7836        let program = Program::parse(initial_source).unwrap().0.unwrap();
7837        let mut frontend = FrontendState::new();
7838
7839        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7840        let mock_ctx = ExecutorContext::new_mock(None).await;
7841        let version = Version(0);
7842
7843        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7844        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7845        let sketch_id = sketch_object.id;
7846        let sketch = expect_sketch(sketch_object);
7847
7848        // Find the circle segment and its start point.
7849        let circle_id = sketch
7850            .segments
7851            .iter()
7852            .copied()
7853            .find(|seg_id| {
7854                matches!(
7855                    &frontend.scene_graph.objects[seg_id.0].kind,
7856                    ObjectKind::Segment {
7857                        segment: Segment::Circle(_)
7858                    }
7859                )
7860            })
7861            .expect("Expected a circle segment in sketch");
7862        let circle_object = &frontend.scene_graph.objects[circle_id.0];
7863        let ObjectKind::Segment {
7864            segment: Segment::Circle(circle),
7865        } = &circle_object.kind
7866        else {
7867            panic!("Expected circle segment, got: {:?}", circle_object.kind);
7868        };
7869        let start_point_id = circle.start;
7870
7871        // Edit the start point via SegmentCtor::Point.
7872        let segments = vec![ExistingSegmentCtor {
7873            id: start_point_id,
7874            ctor: SegmentCtor::Point(PointCtor {
7875                position: Point2d {
7876                    x: Expr::Var(Number {
7877                        value: 7.0,
7878                        units: NumericSuffix::Mm,
7879                    }),
7880                    y: Expr::Var(Number {
7881                        value: 1.0,
7882                        units: NumericSuffix::Mm,
7883                    }),
7884                },
7885            }),
7886        }];
7887        let (src_delta, _scene_delta) = frontend
7888            .edit_segments(&mock_ctx, version, sketch_id, segments)
7889            .await
7890            .unwrap();
7891        assert_eq!(
7892            src_delta.text.as_str(),
7893            "sketch001 = sketch(on = XY) {
7894  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7895}
7896"
7897        );
7898
7899        ctx.close().await;
7900        mock_ctx.close().await;
7901    }
7902
7903    #[tokio::test(flavor = "multi_thread")]
7904    async fn test_add_line_when_sketch_block_uses_variable() {
7905        let initial_source = "s = sketch(on = XY) {}
7906";
7907
7908        let program = Program::parse(initial_source).unwrap().0.unwrap();
7909
7910        let mut frontend = FrontendState::new();
7911
7912        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7913        let mock_ctx = ExecutorContext::new_mock(None).await;
7914        let version = Version(0);
7915
7916        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7917        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7918        let sketch_id = sketch_object.id;
7919
7920        let line_ctor = LineCtor {
7921            start: Point2d {
7922                x: Expr::Number(Number {
7923                    value: 0.0,
7924                    units: NumericSuffix::Mm,
7925                }),
7926                y: Expr::Number(Number {
7927                    value: 0.0,
7928                    units: NumericSuffix::Mm,
7929                }),
7930            },
7931            end: Point2d {
7932                x: Expr::Number(Number {
7933                    value: 10.0,
7934                    units: NumericSuffix::Mm,
7935                }),
7936                y: Expr::Number(Number {
7937                    value: 10.0,
7938                    units: NumericSuffix::Mm,
7939                }),
7940            },
7941            construction: None,
7942        };
7943        let segment = SegmentCtor::Line(line_ctor);
7944        let (src_delta, scene_delta) = frontend
7945            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7946            .await
7947            .unwrap();
7948        assert_eq!(
7949            src_delta.text.as_str(),
7950            "s = sketch(on = XY) {
7951  line(start = [0mm, 0mm], end = [10mm, 10mm])
7952}
7953"
7954        );
7955        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7956        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7957
7958        ctx.close().await;
7959        mock_ctx.close().await;
7960    }
7961
7962    #[tokio::test(flavor = "multi_thread")]
7963    async fn test_new_sketch_add_line_delete_sketch() {
7964        let program = Program::empty();
7965
7966        let mut frontend = FrontendState::new();
7967        frontend.program = program;
7968
7969        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7970        let mock_ctx = ExecutorContext::new_mock(None).await;
7971        let version = Version(0);
7972
7973        let sketch_args = SketchCtor {
7974            on: Plane::Default(PlaneName::Xy),
7975        };
7976        let (_src_delta, scene_delta, sketch_id) = frontend
7977            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7978            .await
7979            .unwrap();
7980        assert_eq!(sketch_id, ObjectId(1));
7981        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7982        let sketch_object = &scene_delta.new_graph.objects[1];
7983        assert_eq!(sketch_object.id, ObjectId(1));
7984        assert_eq!(
7985            sketch_object.kind,
7986            ObjectKind::Sketch(Sketch {
7987                args: SketchCtor {
7988                    on: Plane::Default(PlaneName::Xy)
7989                },
7990                plane: ObjectId(0),
7991                segments: vec![],
7992                constraints: vec![],
7993            })
7994        );
7995        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7996
7997        let line_ctor = LineCtor {
7998            start: Point2d {
7999                x: Expr::Number(Number {
8000                    value: 0.0,
8001                    units: NumericSuffix::Mm,
8002                }),
8003                y: Expr::Number(Number {
8004                    value: 0.0,
8005                    units: NumericSuffix::Mm,
8006                }),
8007            },
8008            end: Point2d {
8009                x: Expr::Number(Number {
8010                    value: 10.0,
8011                    units: NumericSuffix::Mm,
8012                }),
8013                y: Expr::Number(Number {
8014                    value: 10.0,
8015                    units: NumericSuffix::Mm,
8016                }),
8017            },
8018            construction: None,
8019        };
8020        let segment = SegmentCtor::Line(line_ctor);
8021        let (src_delta, scene_delta) = frontend
8022            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8023            .await
8024            .unwrap();
8025        assert_eq!(
8026            src_delta.text.as_str(),
8027            "sketch001 = sketch(on = XY) {
8028  line(start = [0mm, 0mm], end = [10mm, 10mm])
8029}
8030"
8031        );
8032        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8033
8034        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8035        assert_eq!(src_delta.text.as_str(), "");
8036        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8037
8038        ctx.close().await;
8039        mock_ctx.close().await;
8040    }
8041
8042    #[tokio::test(flavor = "multi_thread")]
8043    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8044        let initial_source = "s = sketch(on = XY) {}
8045";
8046
8047        let program = Program::parse(initial_source).unwrap().0.unwrap();
8048
8049        let mut frontend = FrontendState::new();
8050
8051        let ctx = ExecutorContext::new_with_engine(
8052            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8053            Default::default(),
8054        );
8055        let version = Version(0);
8056
8057        frontend.hack_set_program(&ctx, program).await.unwrap();
8058        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8059        let sketch_id = sketch_object.id;
8060
8061        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8062        assert_eq!(src_delta.text.as_str(), "");
8063        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8064
8065        ctx.close().await;
8066    }
8067
8068    #[tokio::test(flavor = "multi_thread")]
8069    async fn test_delete_sketch_after_comment() {
8070        let initial_source = "sketch001 = sketch(on = XZ) {
8071}
8072";
8073
8074        let program = Program::parse(initial_source).unwrap().0.unwrap();
8075        let mut frontend = FrontendState::new();
8076
8077        let ctx = ExecutorContext::new_with_engine(
8078            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8079            Default::default(),
8080        );
8081        let version = Version(0);
8082
8083        frontend.hack_set_program(&ctx, program).await.unwrap();
8084        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8085        let sketch_id = sketch_object.id;
8086        let original_source = sketch_object.source.clone();
8087
8088        let commented_source = "// test 1
8089sketch001 = sketch(on = XZ) {
8090}
8091";
8092        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8093        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8094
8095        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8096        assert_eq!(cached_sketch_object.source, original_source);
8097
8098        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8099        assert!(
8100            !src_delta.text.contains("sketch001"),
8101            "sketch was not deleted: {}",
8102            src_delta.text
8103        );
8104        // The leading line comment must survive deletion.
8105        assert_eq!(src_delta.text.as_str(), "// test 1\n");
8106        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8107
8108        ctx.close().await;
8109    }
8110
8111    #[tokio::test(flavor = "multi_thread")]
8112    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8113        let initial_source = "sketch001 = sketch(on = XZ) {
8114}
8115foo = 1
8116";
8117
8118        let program = Program::parse(initial_source).unwrap().0.unwrap();
8119        let mut frontend = FrontendState::new();
8120
8121        let ctx = ExecutorContext::new_with_engine(
8122            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8123            Default::default(),
8124        );
8125        let version = Version(0);
8126
8127        frontend.hack_set_program(&ctx, program).await.unwrap();
8128        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8129        let sketch_id = sketch_object.id;
8130
8131        let commented_source = "// keep me
8132sketch001 = sketch(on = XZ) {
8133}
8134foo = 1
8135";
8136        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8137        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8138
8139        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8140        // The leading comment should remain, now attached to the following body item.
8141        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8142
8143        ctx.close().await;
8144    }
8145
8146    #[tokio::test(flavor = "multi_thread")]
8147    async fn test_delete_segment_preserves_pre_comment() {
8148        let initial_source = "\
8149sketch(on = XY) {
8150  point(at = [var 1, var 2])
8151  // describe the middle point
8152  point(at = [var 3, var 4])
8153  point(at = [var 5, var 6])
8154}
8155";
8156
8157        let program = Program::parse(initial_source).unwrap().0.unwrap();
8158        let mut frontend = FrontendState::new();
8159
8160        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8161        let mock_ctx = ExecutorContext::new_mock(None).await;
8162        let version = Version(0);
8163
8164        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8165        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8166        let sketch_id = sketch_object.id;
8167        let sketch = expect_sketch(sketch_object);
8168
8169        let middle_point_id = *sketch.segments.get(1).unwrap();
8170
8171        let (src_delta, _scene_delta) = frontend
8172            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8173            .await
8174            .unwrap();
8175        // The line comment on the line above the deleted point must be preserved.
8176        // It is reattached to the next surviving body item.
8177        assert_eq!(
8178            src_delta.text.as_str(),
8179            "\
8180sketch(on = XY) {
8181  point(at = [var 1, var 2])
8182  // describe the middle point
8183  point(at = [var 5, var 6])
8184}
8185"
8186        );
8187
8188        ctx.close().await;
8189        mock_ctx.close().await;
8190    }
8191
8192    #[tokio::test(flavor = "multi_thread")]
8193    async fn test_delete_last_segment_preserves_pre_comment() {
8194        let initial_source = "\
8195sketch(on = XY) {
8196  point(at = [var 1, var 2])
8197  // describe the trailing point
8198  point(at = [var 3, var 4])
8199}
8200";
8201
8202        let program = Program::parse(initial_source).unwrap().0.unwrap();
8203        let mut frontend = FrontendState::new();
8204
8205        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8206        let mock_ctx = ExecutorContext::new_mock(None).await;
8207        let version = Version(0);
8208
8209        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8210        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8211        let sketch_id = sketch_object.id;
8212        let sketch = expect_sketch(sketch_object);
8213
8214        let last_point_id = *sketch.segments.last().unwrap();
8215
8216        let (src_delta, _scene_delta) = frontend
8217            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8218            .await
8219            .unwrap();
8220        // No following item to attach to; the comment is kept inside the sketch
8221        // block as trailing non-code metadata so the user does not lose it.
8222        assert_eq!(
8223            src_delta.text.as_str(),
8224            "\
8225sketch(on = XY) {
8226  point(at = [var 1, var 2])
8227  // describe the trailing point
8228}
8229"
8230        );
8231
8232        ctx.close().await;
8233        mock_ctx.close().await;
8234    }
8235
8236    #[tokio::test(flavor = "multi_thread")]
8237    async fn test_delete_segment_drops_inline_trailing_comment() {
8238        let initial_source = "\
8239sketch(on = XY) {
8240  point(at = [var 1, var 2])
8241  point(at = [var 3, var 4]) // same-line note that gets dropped
8242  point(at = [var 5, var 6])
8243}
8244";
8245
8246        let program = Program::parse(initial_source).unwrap().0.unwrap();
8247        let mut frontend = FrontendState::new();
8248
8249        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8250        let mock_ctx = ExecutorContext::new_mock(None).await;
8251        let version = Version(0);
8252
8253        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8254        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8255        let sketch_id = sketch_object.id;
8256        let sketch = expect_sketch(sketch_object);
8257
8258        let middle_point_id = *sketch.segments.get(1).unwrap();
8259
8260        let (src_delta, _scene_delta) = frontend
8261            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8262            .await
8263            .unwrap();
8264        // The same-line trailing comment is removed along with the deleted code.
8265        assert!(
8266            !src_delta.text.contains("same-line note"),
8267            "inline comment should have been removed: {}",
8268            src_delta.text
8269        );
8270
8271        ctx.close().await;
8272        mock_ctx.close().await;
8273    }
8274
8275    #[tokio::test(flavor = "multi_thread")]
8276    async fn test_delete_segments_preserves_block_comments_across_positions() {
8277        // One test exercising several `delete_body_item_preserving_pre_comments`
8278        // branches at once with `/* ... */` block comments:
8279        //   - first point: leading block comment must migrate to the next item.
8280        //   - first point: same-line trailing block comment must be dropped.
8281        //   - middle point: leading block comment must stay attached after migration.
8282        //   - last point: leading block comment, with no surviving next item,
8283        //     must be converted into a trailing NonCodeNode.
8284        let initial_source = "\
8285sketch(on = XY) {
8286  /* above first - moves to middle */
8287  point(at = [var 1, var 2]) /* same-line on first - dropped */
8288  /* above middle - stays */
8289  point(at = [var 3, var 4])
8290  /* above last - moves to trailing meta */
8291  point(at = [var 5, var 6])
8292}
8293";
8294
8295        let program = Program::parse(initial_source).unwrap().0.unwrap();
8296        let mut frontend = FrontendState::new();
8297
8298        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8299        let mock_ctx = ExecutorContext::new_mock(None).await;
8300        let version = Version(0);
8301
8302        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8304        let sketch_id = sketch_object.id;
8305        let sketch = expect_sketch(sketch_object);
8306
8307        let first_point_id = *sketch.segments.first().unwrap();
8308        let last_point_id = *sketch.segments.last().unwrap();
8309
8310        let (src_delta, _scene_delta) = frontend
8311            .delete_objects(
8312                &mock_ctx,
8313                version,
8314                sketch_id,
8315                Vec::new(),
8316                vec![first_point_id, last_point_id],
8317            )
8318            .await
8319            .unwrap();
8320        assert_eq!(
8321            src_delta.text.as_str(),
8322            "\
8323sketch(on = XY) {
8324  /* above first - moves to middle */
8325  /* above middle - stays */
8326  point(at = [var 3, var 4])
8327  /* above last - moves to trailing meta */
8328}
8329"
8330        );
8331
8332        ctx.close().await;
8333        mock_ctx.close().await;
8334    }
8335
8336    #[tokio::test(flavor = "multi_thread")]
8337    async fn test_edit_line_when_editing_its_start_point() {
8338        let initial_source = "\
8339sketch(on = XY) {
8340  line(start = [var 1, var 2], end = [var 3, var 4])
8341}
8342";
8343
8344        let program = Program::parse(initial_source).unwrap().0.unwrap();
8345
8346        let mut frontend = FrontendState::new();
8347
8348        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8349        let mock_ctx = ExecutorContext::new_mock(None).await;
8350        let version = Version(0);
8351
8352        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8353        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8354        let sketch_id = sketch_object.id;
8355        let sketch = expect_sketch(sketch_object);
8356
8357        let point_id = *sketch.segments.first().unwrap();
8358
8359        let point_ctor = PointCtor {
8360            position: Point2d {
8361                x: Expr::Var(Number {
8362                    value: 5.0,
8363                    units: NumericSuffix::Inch,
8364                }),
8365                y: Expr::Var(Number {
8366                    value: 6.0,
8367                    units: NumericSuffix::Inch,
8368                }),
8369            },
8370        };
8371        let segments = vec![ExistingSegmentCtor {
8372            id: point_id,
8373            ctor: SegmentCtor::Point(point_ctor),
8374        }];
8375        let (src_delta, scene_delta) = frontend
8376            .edit_segments(&mock_ctx, version, sketch_id, segments)
8377            .await
8378            .unwrap();
8379        assert_eq!(
8380            src_delta.text.as_str(),
8381            "\
8382sketch(on = XY) {
8383  line(start = [var 5in, var 6in], end = [var 3, var 4])
8384}
8385"
8386        );
8387        assert_eq!(scene_delta.new_objects, vec![]);
8388        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8389
8390        ctx.close().await;
8391        mock_ctx.close().await;
8392    }
8393
8394    #[tokio::test(flavor = "multi_thread")]
8395    async fn test_edit_line_when_editing_its_end_point() {
8396        let initial_source = "\
8397sketch(on = XY) {
8398  line(start = [var 1, var 2], end = [var 3, var 4])
8399}
8400";
8401
8402        let program = Program::parse(initial_source).unwrap().0.unwrap();
8403
8404        let mut frontend = FrontendState::new();
8405
8406        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8407        let mock_ctx = ExecutorContext::new_mock(None).await;
8408        let version = Version(0);
8409
8410        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8411        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8412        let sketch_id = sketch_object.id;
8413        let sketch = expect_sketch(sketch_object);
8414        let point_id = *sketch.segments.get(1).unwrap();
8415
8416        let point_ctor = PointCtor {
8417            position: Point2d {
8418                x: Expr::Var(Number {
8419                    value: 5.0,
8420                    units: NumericSuffix::Inch,
8421                }),
8422                y: Expr::Var(Number {
8423                    value: 6.0,
8424                    units: NumericSuffix::Inch,
8425                }),
8426            },
8427        };
8428        let segments = vec![ExistingSegmentCtor {
8429            id: point_id,
8430            ctor: SegmentCtor::Point(point_ctor),
8431        }];
8432        let (src_delta, scene_delta) = frontend
8433            .edit_segments(&mock_ctx, version, sketch_id, segments)
8434            .await
8435            .unwrap();
8436        assert_eq!(
8437            src_delta.text.as_str(),
8438            "\
8439sketch(on = XY) {
8440  line(start = [var 1, var 2], end = [var 5in, var 6in])
8441}
8442"
8443        );
8444        assert_eq!(scene_delta.new_objects, vec![]);
8445        assert_eq!(
8446            scene_delta.new_graph.objects.len(),
8447            5,
8448            "{:#?}",
8449            scene_delta.new_graph.objects
8450        );
8451
8452        ctx.close().await;
8453        mock_ctx.close().await;
8454    }
8455
8456    #[tokio::test(flavor = "multi_thread")]
8457    async fn test_edit_line_with_coincident_feedback() {
8458        let initial_source = "\
8459sketch(on = XY) {
8460  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8461  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8462  fixed([line1.start, [0, 0]])
8463  coincident([line1.end, line2.start])
8464  equalLength([line1, line2])
8465}
8466";
8467
8468        let program = Program::parse(initial_source).unwrap().0.unwrap();
8469
8470        let mut frontend = FrontendState::new();
8471
8472        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8473        let mock_ctx = ExecutorContext::new_mock(None).await;
8474        let version = Version(0);
8475
8476        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8477        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8478        let sketch_id = sketch_object.id;
8479        let sketch = expect_sketch(sketch_object);
8480        let line2_end_id = *sketch.segments.get(4).unwrap();
8481
8482        let segments = vec![ExistingSegmentCtor {
8483            id: line2_end_id,
8484            ctor: SegmentCtor::Point(PointCtor {
8485                position: Point2d {
8486                    x: Expr::Var(Number {
8487                        value: 9.0,
8488                        units: NumericSuffix::None,
8489                    }),
8490                    y: Expr::Var(Number {
8491                        value: 10.0,
8492                        units: NumericSuffix::None,
8493                    }),
8494                },
8495            }),
8496        }];
8497        let (src_delta, scene_delta) = frontend
8498            .edit_segments(&mock_ctx, version, sketch_id, segments)
8499            .await
8500            .unwrap();
8501        assert_eq!(
8502            src_delta.text.as_str(),
8503            "\
8504sketch(on = XY) {
8505  line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8506  line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8507  fixed([line1.start, [0, 0]])
8508  coincident([line1.end, line2.start])
8509  equalLength([line1, line2])
8510}
8511"
8512        );
8513        assert_eq!(
8514            scene_delta.new_graph.objects.len(),
8515            11,
8516            "{:#?}",
8517            scene_delta.new_graph.objects
8518        );
8519
8520        ctx.close().await;
8521        mock_ctx.close().await;
8522    }
8523
8524    #[tokio::test(flavor = "multi_thread")]
8525    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8526        let initial_source = "\
8527sketch(on = XY) {
8528  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8529  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8530  fixed([line1.start, [0, 0]])
8531  coincident([line1.end, line2.start])
8532  equalLength([line1, line2])
8533}
8534";
8535
8536        let program = Program::parse(initial_source).unwrap().0.unwrap();
8537        let mut frontend = FrontendState::new();
8538        let mock_ctx = ExecutorContext::new_mock(None).await;
8539        let version = Version(0);
8540
8541        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8542        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8543        let sketch_id = sketch_object.id;
8544        let sketch = expect_sketch(sketch_object);
8545        let line2_end_id = *sketch.segments.get(4).unwrap();
8546
8547        let segments = vec![ExistingSegmentCtor {
8548            id: line2_end_id,
8549            ctor: SegmentCtor::Point(PointCtor {
8550                position: Point2d {
8551                    x: Expr::Var(Number {
8552                        value: 9.0,
8553                        units: NumericSuffix::None,
8554                    }),
8555                    y: Expr::Var(Number {
8556                        value: 10.0,
8557                        units: NumericSuffix::None,
8558                    }),
8559                },
8560            }),
8561        }];
8562        let (edited_source, _) = frontend
8563            .edit_segments(&mock_ctx, version, sketch_id, segments)
8564            .await
8565            .unwrap();
8566
8567        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8568        assert_eq!(mock_source.text, edited_source.text);
8569
8570        mock_ctx.close().await;
8571    }
8572
8573    /// Preview segment edits should return solved geometry without persisting
8574    /// solver feedback to KCL.
8575    #[tokio::test(flavor = "multi_thread")]
8576    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8577        let initial_source = "\
8578sketch(on = XY) {
8579  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8580  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8581  fixed([line1.start, [0, 0]])
8582  coincident([line1.end, line2.start])
8583  equalLength([line1, line2])
8584}
8585";
8586
8587        let program = Program::parse(initial_source).unwrap().0.unwrap();
8588        let mut frontend = FrontendState::new();
8589        let mock_ctx = ExecutorContext::new_mock(None).await;
8590        let version = Version(0);
8591
8592        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594        let sketch_id = sketch_object.id;
8595        let sketch = expect_sketch(sketch_object);
8596        let line2_end_id = *sketch.segments.get(4).unwrap();
8597
8598        let segments = vec![ExistingSegmentCtor {
8599            id: line2_end_id,
8600            ctor: SegmentCtor::Point(PointCtor {
8601                position: Point2d {
8602                    x: Expr::Var(Number {
8603                        value: 9.0,
8604                        units: NumericSuffix::None,
8605                    }),
8606                    y: Expr::Var(Number {
8607                        value: 10.0,
8608                        units: NumericSuffix::None,
8609                    }),
8610                },
8611            }),
8612        }];
8613        let (preview_source, preview_delta) = frontend
8614            .edit_segments_with_options(
8615                &mock_ctx,
8616                version,
8617                sketch_id,
8618                segments,
8619                EditSegmentsOptions {
8620                    anchor_segment_ids: Some(vec![line2_end_id]),
8621                    drag_anchors: Vec::new(),
8622                    commit_solved_initial_guesses: false,
8623                },
8624            )
8625            .await
8626            .unwrap();
8627
8628        assert!(
8629            !preview_delta.exec_outcome.var_solutions.is_empty(),
8630            "preview solve should still solve and return geometry feedback"
8631        );
8632        assert!(
8633            preview_source
8634                .text
8635                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8636        );
8637        assert!(
8638            preview_source
8639                .text
8640                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8641        );
8642
8643        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8644        assert_eq!(mock_source.text, preview_source.text);
8645
8646        mock_ctx.close().await;
8647    }
8648
8649    #[tokio::test(flavor = "multi_thread")]
8650    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8651        let initial_source = "\
8652sketch(on = XY) {
8653  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8654}
8655";
8656
8657        let program = Program::parse(initial_source).unwrap().0.unwrap();
8658        let mut frontend = FrontendState::new();
8659        let mock_ctx = ExecutorContext::new_mock(None).await;
8660        let version = Version(0);
8661
8662        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8663        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8664        let sketch_id = sketch_object.id;
8665        let sketch = expect_sketch(sketch_object);
8666        let line_end_id = *sketch.segments.get(1).unwrap();
8667
8668        let constraint = Constraint::Fixed(Fixed {
8669            points: vec![FixedPoint {
8670                point: line_end_id,
8671                position: Point2d {
8672                    x: Number {
8673                        value: 20.0,
8674                        units: NumericSuffix::Mm,
8675                    },
8676                    y: Number {
8677                        value: 0.0,
8678                        units: NumericSuffix::Mm,
8679                    },
8680                },
8681            }],
8682        });
8683        let (constraint_source, _) = frontend
8684            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8685            .await
8686            .unwrap();
8687
8688        assert!(
8689            constraint_source
8690                .text
8691                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8692            "{}",
8693            constraint_source.text
8694        );
8695        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8696        assert_eq!(mock_source.text, constraint_source.text);
8697
8698        mock_ctx.close().await;
8699    }
8700
8701    #[test]
8702    fn test_no_solver_feedback_preserves_original_source() {
8703        let initial_source = "\
8704@settings(defaultLengthUnit = in, kclVersion = 2.0)
8705cylinder = startSketchOn(XY)
8706    |> circle(center= [0, 0], radius= 22)
8707    |> extrude(length = 14)
8708";
8709        let mut frontend = FrontendState::new();
8710        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8711        let outcome = ExecOutcome {
8712            variables: Default::default(),
8713            operations: Default::default(),
8714            artifact_graph: Default::default(),
8715            scene_objects: Default::default(),
8716            source_range_to_object: Default::default(),
8717            var_solutions: Default::default(),
8718            issues: Default::default(),
8719            filenames: Default::default(),
8720            default_planes: Default::default(),
8721        };
8722
8723        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8724
8725        assert_eq!(source_delta.text, initial_source);
8726    }
8727
8728    /// Explicit drag anchors should limit which edited points become temporary
8729    /// fixed constraints.
8730    #[tokio::test(flavor = "multi_thread")]
8731    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8732        let initial_source = "\
8733sketch(on = XY) {
8734  point1 = point(at = [var 0mm, var 0mm])
8735  point2 = point(at = [var 0mm, var 0mm])
8736  coincident([point1, point2])
8737}
8738";
8739
8740        let program = Program::parse(initial_source).unwrap().0.unwrap();
8741        let mut frontend = FrontendState::new();
8742        let mock_ctx = ExecutorContext::new_mock(None).await;
8743        let version = Version(0);
8744
8745        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8746        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8747        let sketch_id = sketch_object.id;
8748        let sketch = expect_sketch(sketch_object);
8749        let point1_id = sketch.segments[0];
8750        let point2_id = sketch.segments[1];
8751
8752        let segments = vec![
8753            ExistingSegmentCtor {
8754                id: point1_id,
8755                ctor: SegmentCtor::Point(PointCtor {
8756                    position: point_expr_mm(10.0, 0.0),
8757                }),
8758            },
8759            ExistingSegmentCtor {
8760                id: point2_id,
8761                ctor: SegmentCtor::Point(PointCtor {
8762                    position: point_expr_mm(100.0, 0.0),
8763                }),
8764            },
8765        ];
8766        let (_, scene_delta) = frontend
8767            .edit_segments_with_options(
8768                &mock_ctx,
8769                version,
8770                sketch_id,
8771                segments,
8772                EditSegmentsOptions {
8773                    anchor_segment_ids: Some(vec![point1_id]),
8774                    drag_anchors: Vec::new(),
8775                    commit_solved_initial_guesses: true,
8776                },
8777            )
8778            .await
8779            .unwrap();
8780
8781        assert_point_position_close(
8782            point_position(&scene_delta.new_graph, point1_id),
8783            point_number_mm(10.0, 0.0),
8784        );
8785        assert_point_position_close(
8786            point_position(&scene_delta.new_graph, point2_id),
8787            point_number_mm(10.0, 0.0),
8788        );
8789
8790        mock_ctx.close().await;
8791    }
8792
8793    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
8794    /// for every SketchVar whose initial NumericLiteral has the given value.
8795    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8796        use std::cell::RefCell;
8797        struct Collector {
8798            target: f64,
8799            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8800        }
8801        impl<'a> crate::walk::Visitor<'a> for &Collector {
8802            type Error = crate::front::Error;
8803            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8804                if let crate::walk::Node::SketchVar(sketch_var) = node
8805                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8806                    && (initial.value - self.target).abs() < 1e-9
8807                {
8808                    self.out
8809                        .borrow_mut()
8810                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8811                }
8812                for child in node.children().iter() {
8813                    if !child.visit(*self)? {
8814                        return Ok(false);
8815                    }
8816                }
8817                Ok(true)
8818            }
8819        }
8820        let collector = Collector {
8821            target: value,
8822            out: Default::default(),
8823        };
8824        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8825        collector.out.into_inner()
8826    }
8827
8828    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
8829    /// for every SketchVar (including bare `var`).
8830    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8831        use std::cell::RefCell;
8832        struct Collector {
8833            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8834        }
8835        impl<'a> crate::walk::Visitor<'a> for &Collector {
8836            type Error = crate::front::Error;
8837            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8838                if let crate::walk::Node::SketchVar(sketch_var) = node
8839                    && let Some(node_path) = &sketch_var.node_path
8840                {
8841                    self.out
8842                        .borrow_mut()
8843                        .push((SourceRange::from(sketch_var), node_path.clone()));
8844                }
8845                for child in node.children().iter() {
8846                    if !child.visit(*self)? {
8847                        return Ok(false);
8848                    }
8849                }
8850                Ok(true)
8851            }
8852        }
8853        let collector = Collector {
8854            out: Default::default(),
8855        };
8856        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8857        collector.out.into_inner()
8858    }
8859
8860    fn empty_exec_outcome_with_var_solutions(
8861        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8862    ) -> ExecOutcome {
8863        ExecOutcome {
8864            variables: Default::default(),
8865            operations: Default::default(),
8866            artifact_graph: Default::default(),
8867            scene_objects: Default::default(),
8868            source_range_to_object: Default::default(),
8869            var_solutions,
8870            issues: Default::default(),
8871            filenames: Default::default(),
8872            default_planes: Default::default(),
8873        }
8874    }
8875
8876    /// Happy path: commit a var solution to a `var N` inside a sketch block
8877    /// using a correct NodePath. Confirms the node-path code path produces the
8878    /// expected source mutation.
8879    #[test]
8880    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
8881        let initial_source = "\
8882sketch(on = XY) {
8883  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8884}
8885";
8886        let program = Program::parse(initial_source).unwrap().0.unwrap();
8887        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
8888        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
8889        let (literal_range, node_path) = matches.into_iter().next().unwrap();
8890
8891        let mut frontend = FrontendState::new();
8892        frontend.program = program;
8893
8894        let outcome = empty_exec_outcome_with_var_solutions(vec![(
8895            literal_range,
8896            Some(node_path),
8897            Number {
8898                value: 25.0,
8899                units: NumericSuffix::Mm,
8900            },
8901        )]);
8902
8903        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8904
8905        assert_eq!(
8906            source_delta.text,
8907            "\
8908sketch(on = XY) {
8909  line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
8910}
8911",
8912        );
8913    }
8914
8915    /// Whitespace inserted earlier in the source shifts the original SketchVar
8916    /// SourceRange. With NodePath propagation the commit should still target
8917    /// the right `var`. We simulate this by collecting node_paths against a
8918    /// "compact" source, then loading the frontend with a "padded" source
8919    /// (whose byte offsets differ), and feeding the original (now stale)
8920    /// source range plus the correct node_path back into the commit.
8921    #[test]
8922    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
8923        let compact_source = "\
8924sketch(on = XY) {
8925  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8926}
8927";
8928        let padded_source = "\
8929// added comment\n// added comment\n\nsketch(on = XY) {
8930  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8931}
8932";
8933        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
8934        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
8935
8936        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
8937            .into_iter()
8938            .next()
8939            .expect("expected `var 10mm` in compact source");
8940        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
8941            .into_iter()
8942            .next()
8943            .expect("expected `var 10mm` in padded source");
8944
8945        assert_ne!(
8946            compact_match.0, padded_match.0,
8947            "byte offsets must differ for this test to be meaningful"
8948        );
8949        assert_eq!(
8950            compact_match.1, padded_match.1,
8951            "node paths must agree across whitespace; that's the whole point of NodePath",
8952        );
8953
8954        let mut frontend = FrontendState::new();
8955        frontend.program = padded_program;
8956
8957        // Stale source range from the compact source + correct node_path.
8958        let outcome = empty_exec_outcome_with_var_solutions(vec![(
8959            compact_match.0,
8960            Some(compact_match.1),
8961            Number {
8962                value: 30.0,
8963                units: NumericSuffix::Mm,
8964            },
8965        )]);
8966
8967        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8968
8969        assert_eq!(
8970            source_delta.text,
8971            "\
8972// added comment
8973// added comment
8974
8975sketch(on = XY) {
8976  line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
8977}
8978",
8979        );
8980    }
8981
8982    /// When multiple `var` declarations exist and the stale source range
8983    /// happens to land on a *different* var, the node_path must take
8984    /// precedence and the right var gets updated.
8985    #[test]
8986    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
8987        let initial_source = "\
8988sketch(on = XY) {
8989  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
8990}
8991";
8992        let program = Program::parse(initial_source).unwrap().0.unwrap();
8993
8994        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
8995            .into_iter()
8996            .next()
8997            .expect("expected `var 10mm`");
8998        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
8999            .into_iter()
9000            .next()
9001            .expect("expected `var 20mm`");
9002
9003        let mut frontend = FrontendState::new();
9004        frontend.program = program;
9005
9006        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9007        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9008            var_20.0,
9009            Some(var_10.1),
9010            Number {
9011                value: 33.0,
9012                units: NumericSuffix::Mm,
9013            },
9014        )]);
9015
9016        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9017
9018        assert_eq!(
9019            source_delta.text,
9020            "\
9021sketch(on = XY) {
9022  line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9023}
9024",
9025        );
9026    }
9027
9028    /// Bare `var` (no initial literal) is only locatable via node_path. With
9029    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9030    /// solver solution should fill the initial value in. The
9031    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9032    /// is gated as an experimental feature; without it the re-parse of the
9033    /// recast source rejects bare `var` declarations.
9034    #[test]
9035    fn test_commit_var_solution_writes_back_into_bare_var() {
9036        let initial_source = "\
9037@settings(experimentalFeatures = allow, kclVersion = 2.0)
9038sketch(on = XY) {
9039  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9040}
9041";
9042        let program = Program::parse(initial_source).unwrap().0.unwrap();
9043
9044        // Pick the first bare `var`; collect_all_sketch_vars returns every
9045        // SketchVar, including bare ones.
9046        let bare = collect_all_sketch_vars(&program)
9047            .into_iter()
9048            .find(|(range, _)| {
9049                // The bare `var` is exactly the 3 characters "var".
9050                range.end() - range.start() == 3
9051            })
9052            .expect("expected at least one bare `var`");
9053
9054        let mut frontend = FrontendState::new();
9055        frontend.program = program;
9056
9057        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9058            bare.0,
9059            Some(bare.1),
9060            Number {
9061                value: 7.0,
9062                units: NumericSuffix::Mm,
9063            },
9064        )]);
9065
9066        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9067
9068        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9069        // written as an explicit suffix so the bare var commits with units.
9070        // The recast adds a blank line after the `@settings` annotation.
9071        assert_eq!(
9072            source_delta.text,
9073            "\
9074@settings(experimentalFeatures = allow, kclVersion = 2.0)
9075
9076sketch(on = XY) {
9077  line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9078}
9079",
9080        );
9081    }
9082
9083    #[tokio::test(flavor = "multi_thread")]
9084    async fn test_delete_point_without_var() {
9085        let initial_source = "\
9086sketch(on = XY) {
9087  point(at = [var 1, var 2])
9088  point(at = [var 3, var 4])
9089  point(at = [var 5, var 6])
9090}
9091";
9092
9093        let program = Program::parse(initial_source).unwrap().0.unwrap();
9094
9095        let mut frontend = FrontendState::new();
9096
9097        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9098        let mock_ctx = ExecutorContext::new_mock(None).await;
9099        let version = Version(0);
9100
9101        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9102        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9103        let sketch_id = sketch_object.id;
9104        let sketch = expect_sketch(sketch_object);
9105
9106        let point_id = *sketch.segments.get(1).unwrap();
9107
9108        let (src_delta, scene_delta) = frontend
9109            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9110            .await
9111            .unwrap();
9112        assert_eq!(
9113            src_delta.text.as_str(),
9114            "\
9115sketch(on = XY) {
9116  point(at = [var 1, var 2])
9117  point(at = [var 5, var 6])
9118}
9119"
9120        );
9121        assert_eq!(scene_delta.new_objects, vec![]);
9122        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9123
9124        ctx.close().await;
9125        mock_ctx.close().await;
9126    }
9127
9128    #[tokio::test(flavor = "multi_thread")]
9129    async fn test_delete_point_with_var() {
9130        let initial_source = "\
9131sketch(on = XY) {
9132  point(at = [var 1, var 2])
9133  point1 = point(at = [var 3, var 4])
9134  point(at = [var 5, var 6])
9135}
9136";
9137
9138        let program = Program::parse(initial_source).unwrap().0.unwrap();
9139
9140        let mut frontend = FrontendState::new();
9141
9142        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9143        let mock_ctx = ExecutorContext::new_mock(None).await;
9144        let version = Version(0);
9145
9146        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9147        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9148        let sketch_id = sketch_object.id;
9149        let sketch = expect_sketch(sketch_object);
9150
9151        let point_id = *sketch.segments.get(1).unwrap();
9152
9153        let (src_delta, scene_delta) = frontend
9154            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9155            .await
9156            .unwrap();
9157        assert_eq!(
9158            src_delta.text.as_str(),
9159            "\
9160sketch(on = XY) {
9161  point(at = [var 1, var 2])
9162  point(at = [var 5, var 6])
9163}
9164"
9165        );
9166        assert_eq!(scene_delta.new_objects, vec![]);
9167        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9168
9169        ctx.close().await;
9170        mock_ctx.close().await;
9171    }
9172
9173    #[tokio::test(flavor = "multi_thread")]
9174    async fn test_delete_multiple_points() {
9175        let initial_source = "\
9176sketch(on = XY) {
9177  point(at = [var 1, var 2])
9178  point1 = point(at = [var 3, var 4])
9179  point(at = [var 5, var 6])
9180}
9181";
9182
9183        let program = Program::parse(initial_source).unwrap().0.unwrap();
9184
9185        let mut frontend = FrontendState::new();
9186
9187        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9188        let mock_ctx = ExecutorContext::new_mock(None).await;
9189        let version = Version(0);
9190
9191        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9192        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9193        let sketch_id = sketch_object.id;
9194
9195        let sketch = expect_sketch(sketch_object);
9196
9197        let point1_id = *sketch.segments.first().unwrap();
9198        let point2_id = *sketch.segments.get(1).unwrap();
9199
9200        let (src_delta, scene_delta) = frontend
9201            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9202            .await
9203            .unwrap();
9204        assert_eq!(
9205            src_delta.text.as_str(),
9206            "\
9207sketch(on = XY) {
9208  point(at = [var 5, var 6])
9209}
9210"
9211        );
9212        assert_eq!(scene_delta.new_objects, vec![]);
9213        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9214
9215        ctx.close().await;
9216        mock_ctx.close().await;
9217    }
9218
9219    #[tokio::test(flavor = "multi_thread")]
9220    async fn test_delete_coincident_constraint() {
9221        let initial_source = "\
9222sketch(on = XY) {
9223  point1 = point(at = [var 1, var 2])
9224  point2 = point(at = [var 3, var 4])
9225  coincident([point1, point2])
9226  point(at = [var 5, var 6])
9227}
9228";
9229
9230        let program = Program::parse(initial_source).unwrap().0.unwrap();
9231
9232        let mut frontend = FrontendState::new();
9233
9234        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9235        let mock_ctx = ExecutorContext::new_mock(None).await;
9236        let version = Version(0);
9237
9238        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9239        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9240        let sketch_id = sketch_object.id;
9241        let sketch = expect_sketch(sketch_object);
9242
9243        let coincident_id = *sketch.constraints.first().unwrap();
9244
9245        let (src_delta, scene_delta) = frontend
9246            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9247            .await
9248            .unwrap();
9249        assert_eq!(
9250            src_delta.text.as_str(),
9251            "\
9252sketch(on = XY) {
9253  point1 = point(at = [var 1, var 2])
9254  point2 = point(at = [var 3, var 4])
9255  point(at = [var 5, var 6])
9256}
9257"
9258        );
9259        assert_eq!(scene_delta.new_objects, vec![]);
9260        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9261
9262        ctx.close().await;
9263        mock_ctx.close().await;
9264    }
9265
9266    #[tokio::test(flavor = "multi_thread")]
9267    async fn test_delete_line_cascades_to_coincident_constraint() {
9268        let initial_source = "\
9269sketch(on = XY) {
9270  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9271  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9272  coincident([line1.end, line2.start])
9273}
9274";
9275
9276        let program = Program::parse(initial_source).unwrap().0.unwrap();
9277
9278        let mut frontend = FrontendState::new();
9279
9280        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9281        let mock_ctx = ExecutorContext::new_mock(None).await;
9282        let version = Version(0);
9283
9284        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9285        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9286        let sketch_id = sketch_object.id;
9287        let sketch = expect_sketch(sketch_object);
9288        let line_id = *sketch.segments.get(5).unwrap();
9289
9290        let (src_delta, scene_delta) = frontend
9291            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9292            .await
9293            .unwrap();
9294        assert_eq!(
9295            src_delta.text.as_str(),
9296            "\
9297sketch(on = XY) {
9298  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9299}
9300"
9301        );
9302        assert_eq!(
9303            scene_delta.new_graph.objects.len(),
9304            5,
9305            "{:#?}",
9306            scene_delta.new_graph.objects
9307        );
9308
9309        ctx.close().await;
9310        mock_ctx.close().await;
9311    }
9312
9313    #[tokio::test(flavor = "multi_thread")]
9314    async fn test_delete_line_cascades_to_distance_constraint() {
9315        let initial_source = "\
9316sketch(on = XY) {
9317  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9318  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9319  distance([line1.end, line2.start]) == 10mm
9320}
9321";
9322
9323        let program = Program::parse(initial_source).unwrap().0.unwrap();
9324
9325        let mut frontend = FrontendState::new();
9326
9327        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9328        let mock_ctx = ExecutorContext::new_mock(None).await;
9329        let version = Version(0);
9330
9331        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9332        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9333        let sketch_id = sketch_object.id;
9334        let sketch = expect_sketch(sketch_object);
9335        let line_id = *sketch.segments.get(5).unwrap();
9336
9337        let (src_delta, scene_delta) = frontend
9338            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9339            .await
9340            .unwrap();
9341        assert_eq!(
9342            src_delta.text.as_str(),
9343            "\
9344sketch(on = XY) {
9345  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9346}
9347"
9348        );
9349        assert_eq!(
9350            scene_delta.new_graph.objects.len(),
9351            5,
9352            "{:#?}",
9353            scene_delta.new_graph.objects
9354        );
9355
9356        ctx.close().await;
9357        mock_ctx.close().await;
9358    }
9359
9360    #[tokio::test(flavor = "multi_thread")]
9361    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9362        let initial_source = "\
9363sketch(on = XY) {
9364  point1 = point(at = [var 1, var 2])
9365  point2 = point(at = [var 3, var 4])
9366  horizontalDistance([point1, point2]) == 10mm
9367}
9368";
9369
9370        let program = Program::parse(initial_source).unwrap().0.unwrap();
9371
9372        let mut frontend = FrontendState::new();
9373
9374        let mock_ctx = ExecutorContext::new_mock(None).await;
9375        let version = Version(0);
9376
9377        frontend.program = program.clone();
9378        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9379        frontend.update_state_after_exec(outcome, true);
9380        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9381        let sketch_id = sketch_object.id;
9382        let sketch = expect_sketch(sketch_object);
9383        let point2_id = *sketch.segments.get(1).unwrap();
9384
9385        let (src_delta, scene_delta) = frontend
9386            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9387            .await
9388            .unwrap();
9389        assert_eq!(
9390            src_delta.text.as_str(),
9391            "\
9392sketch(on = XY) {
9393  point1 = point(at = [var 1, var 2])
9394}
9395"
9396        );
9397        assert_eq!(
9398            scene_delta.new_graph.objects.len(),
9399            3,
9400            "{:#?}",
9401            scene_delta.new_graph.objects
9402        );
9403
9404        mock_ctx.close().await;
9405    }
9406
9407    #[tokio::test(flavor = "multi_thread")]
9408    async fn test_delete_line_cascades_to_fixed_constraint() {
9409        let initial_source = "\
9410sketch(on = XY) {
9411  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9412  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9413  fixed([line1.start, [0, 0]])
9414}
9415";
9416
9417        let program = Program::parse(initial_source).unwrap().0.unwrap();
9418
9419        let mut frontend = FrontendState::new();
9420
9421        let mock_ctx = ExecutorContext::new_mock(None).await;
9422        let version = Version(0);
9423
9424        frontend.program = program.clone();
9425        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9426        frontend.update_state_after_exec(outcome, true);
9427        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9428        let sketch_id = sketch_object.id;
9429        let sketch = expect_sketch(sketch_object);
9430        let line1_id = *sketch.segments.get(2).unwrap();
9431
9432        let (src_delta, scene_delta) = frontend
9433            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9434            .await
9435            .unwrap();
9436        assert_eq!(
9437            src_delta.text.as_str(),
9438            "\
9439sketch(on = XY) {
9440  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9441}
9442"
9443        );
9444        assert_eq!(
9445            scene_delta.new_graph.objects.len(),
9446            5,
9447            "{:#?}",
9448            scene_delta.new_graph.objects
9449        );
9450
9451        mock_ctx.close().await;
9452    }
9453
9454    #[tokio::test(flavor = "multi_thread")]
9455    async fn test_delete_line_cascades_to_midpoint_constraint() {
9456        let initial_source = "\
9457sketch(on = XY) {
9458  point1 = point(at = [var 1, var 2])
9459  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9460  midpoint(line1, point = point1)
9461}
9462";
9463
9464        let program = Program::parse(initial_source).unwrap().0.unwrap();
9465
9466        let mut frontend = FrontendState::new();
9467
9468        let mock_ctx = ExecutorContext::new_mock(None).await;
9469        let version = Version(0);
9470
9471        frontend.program = program.clone();
9472        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9473        frontend.update_state_after_exec(outcome, true);
9474        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9475        let sketch_id = sketch_object.id;
9476        let sketch = expect_sketch(sketch_object);
9477        let line1_id = *sketch.segments.get(3).unwrap();
9478
9479        let (src_delta, scene_delta) = frontend
9480            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9481            .await
9482            .unwrap();
9483        assert_eq!(
9484            src_delta.text.as_str(),
9485            "\
9486sketch(on = XY) {
9487  point1 = point(at = [var 1, var 2])
9488}
9489"
9490        );
9491        assert_eq!(
9492            scene_delta.new_graph.objects.len(),
9493            3,
9494            "{:#?}",
9495            scene_delta.new_graph.objects
9496        );
9497
9498        mock_ctx.close().await;
9499    }
9500
9501    #[tokio::test(flavor = "multi_thread")]
9502    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9503        let initial_source = "\
9504sketch(on = XY) {
9505  point1 = point(at = [var 1, var 2])
9506  point2 = point(at = [var 3, var 4])
9507  point3 = point(at = [var 5, var 6])
9508  coincident([point1, point2, point3])
9509}
9510";
9511
9512        let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514        let mut frontend = FrontendState::new();
9515
9516        let mock_ctx = ExecutorContext::new_mock(None).await;
9517        let version = Version(0);
9518
9519        frontend.program = program.clone();
9520        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521        frontend.update_state_after_exec(outcome, true);
9522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523        let sketch_id = sketch_object.id;
9524        let sketch = expect_sketch(sketch_object);
9525        let point3_id = *sketch.segments.get(2).unwrap();
9526
9527        let (src_delta, scene_delta) = frontend
9528            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9529            .await
9530            .unwrap();
9531        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9532        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9533        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9534        assert!(
9535            src_delta.text.contains("coincident([point1, point2])"),
9536            "{}",
9537            src_delta.text
9538        );
9539
9540        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9541        let sketch = expect_sketch(sketch_object);
9542        assert_eq!(sketch.segments.len(), 2);
9543        assert_eq!(sketch.constraints.len(), 1);
9544
9545        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9546        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9547            panic!("Expected constraint object");
9548        };
9549        let Constraint::Coincident(coincident) = constraint else {
9550            panic!("Expected coincident constraint");
9551        };
9552        assert_eq!(
9553            coincident.segments,
9554            sketch
9555                .segments
9556                .iter()
9557                .copied()
9558                .map(Into::into)
9559                .collect::<Vec<ConstraintSegment>>()
9560        );
9561
9562        mock_ctx.close().await;
9563    }
9564
9565    #[tokio::test(flavor = "multi_thread")]
9566    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9567        let initial_source = "\
9568sketch(on = XY) {
9569  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9570  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9571  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9572  equalLength([line1, line2, line3])
9573}
9574";
9575
9576        let program = Program::parse(initial_source).unwrap().0.unwrap();
9577
9578        let mut frontend = FrontendState::new();
9579
9580        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9581        let mock_ctx = ExecutorContext::new_mock(None).await;
9582        let version = Version(0);
9583
9584        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9585        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9586        let sketch_id = sketch_object.id;
9587        let sketch = expect_sketch(sketch_object);
9588        let line3_id = *sketch.segments.get(8).unwrap();
9589
9590        let (src_delta, scene_delta) = frontend
9591            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9592            .await
9593            .unwrap();
9594        assert_eq!(
9595            src_delta.text.as_str(),
9596            "\
9597sketch(on = XY) {
9598  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9599  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9600  equalLength([line1, line2])
9601}
9602"
9603        );
9604
9605        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9606        let sketch = expect_sketch(sketch_object);
9607        assert_eq!(sketch.constraints.len(), 1);
9608
9609        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9610        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9611            panic!("Expected constraint object");
9612        };
9613        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9614            panic!("Expected lines equal length constraint");
9615        };
9616        assert_eq!(lines_equal_length.lines.len(), 2);
9617
9618        ctx.close().await;
9619        mock_ctx.close().await;
9620    }
9621
9622    #[tokio::test(flavor = "multi_thread")]
9623    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9624        let initial_source = "\
9625sketch(on = XY) {
9626  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9627  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9628  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9629  horizontal([line1.end, line2.start, line3.start])
9630}
9631";
9632
9633        let program = Program::parse(initial_source).unwrap().0.unwrap();
9634
9635        let mut frontend = FrontendState::new();
9636
9637        let mock_ctx = ExecutorContext::new_mock(None).await;
9638        let version = Version(0);
9639
9640        frontend.program = program.clone();
9641        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9642        frontend.update_state_after_exec(outcome, true);
9643        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9644        let sketch_id = sketch_object.id;
9645        let sketch = expect_sketch(sketch_object);
9646        let line1_id = *sketch.segments.get(2).unwrap();
9647
9648        let (src_delta, scene_delta) = frontend
9649            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9650            .await
9651            .unwrap();
9652        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9653        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9654        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9655        assert!(
9656            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9657            "{}",
9658            src_delta.text
9659        );
9660
9661        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9662        let sketch = expect_sketch(sketch_object);
9663        assert_eq!(sketch.constraints.len(), 1);
9664
9665        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9666        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9667            panic!("Expected constraint object");
9668        };
9669        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9670            panic!("Expected horizontal points constraint");
9671        };
9672        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9673        assert_eq!(*points, remaining_points);
9674
9675        mock_ctx.close().await;
9676    }
9677
9678    #[tokio::test(flavor = "multi_thread")]
9679    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9680        let initial_source = "\
9681sketch(on = XY) {
9682  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9683  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9684  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9685  vertical([line1.end, line2.start, line3.start])
9686}
9687";
9688
9689        let program = Program::parse(initial_source).unwrap().0.unwrap();
9690
9691        let mut frontend = FrontendState::new();
9692
9693        let mock_ctx = ExecutorContext::new_mock(None).await;
9694        let version = Version(0);
9695
9696        frontend.program = program.clone();
9697        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9698        frontend.update_state_after_exec(outcome, true);
9699        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9700        let sketch_id = sketch_object.id;
9701        let sketch = expect_sketch(sketch_object);
9702        let line1_id = *sketch.segments.get(2).unwrap();
9703
9704        let (src_delta, scene_delta) = frontend
9705            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9706            .await
9707            .unwrap();
9708        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9709        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9710        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9711        assert!(
9712            src_delta.text.contains("vertical([line2.start, line3.start])"),
9713            "{}",
9714            src_delta.text
9715        );
9716
9717        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9718        let sketch = expect_sketch(sketch_object);
9719        assert_eq!(sketch.constraints.len(), 1);
9720
9721        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9722        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9723            panic!("Expected constraint object");
9724        };
9725        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9726            panic!("Expected vertical points constraint");
9727        };
9728        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9729        assert_eq!(*points, remaining_points);
9730
9731        mock_ctx.close().await;
9732    }
9733
9734    #[tokio::test(flavor = "multi_thread")]
9735    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9736        let initial_source = "\
9737sketch(on = XY) {
9738  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9739  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9740  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9741  coincident([line1.end, line2.start, line3.start])
9742}
9743";
9744
9745        let program = Program::parse(initial_source).unwrap().0.unwrap();
9746
9747        let mut frontend = FrontendState::new();
9748
9749        let mock_ctx = ExecutorContext::new_mock(None).await;
9750        let version = Version(0);
9751
9752        frontend.program = program.clone();
9753        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9754        frontend.update_state_after_exec(outcome, true);
9755        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9756        let sketch_id = sketch_object.id;
9757        let sketch = expect_sketch(sketch_object);
9758        let line1_id = *sketch.segments.get(2).unwrap();
9759
9760        let (src_delta, scene_delta) = frontend
9761            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9762            .await
9763            .unwrap();
9764        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9765        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9766        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9767        assert!(
9768            src_delta.text.contains("coincident([line2.start, line3.start])"),
9769            "{}",
9770            src_delta.text
9771        );
9772
9773        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9774        let sketch = expect_sketch(sketch_object);
9775        assert_eq!(sketch.constraints.len(), 1);
9776
9777        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9778        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9779            panic!("Expected constraint object");
9780        };
9781        let Constraint::Coincident(coincident) = constraint else {
9782            panic!("Expected coincident constraint");
9783        };
9784        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9785        assert_eq!(coincident.segments, remaining_segments);
9786
9787        mock_ctx.close().await;
9788    }
9789
9790    #[tokio::test(flavor = "multi_thread")]
9791    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9792        let initial_source = "\
9793sketch(on = XY) {
9794  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9795  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9796  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9797  equalLength([line1, line2, line3])
9798}
9799";
9800
9801        let program = Program::parse(initial_source).unwrap().0.unwrap();
9802
9803        let mut frontend = FrontendState::new();
9804
9805        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9806        let mock_ctx = ExecutorContext::new_mock(None).await;
9807        let version = Version(0);
9808
9809        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9810        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9811        let sketch_id = sketch_object.id;
9812        let sketch = expect_sketch(sketch_object);
9813        let line2_id = *sketch.segments.get(5).unwrap();
9814        let line3_id = *sketch.segments.get(8).unwrap();
9815
9816        let (src_delta, scene_delta) = frontend
9817            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9818            .await
9819            .unwrap();
9820        assert_eq!(
9821            src_delta.text.as_str(),
9822            "\
9823sketch(on = XY) {
9824  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9825}
9826"
9827        );
9828
9829        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9830        let sketch = expect_sketch(sketch_object);
9831        assert!(sketch.constraints.is_empty());
9832
9833        ctx.close().await;
9834        mock_ctx.close().await;
9835    }
9836
9837    #[tokio::test(flavor = "multi_thread")]
9838    async fn test_delete_line_preserves_multiline_parallel_constraint() {
9839        let initial_source = "\
9840sketch(on = XY) {
9841  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9842  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9843  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9844  parallel([line1, line2, line3])
9845}
9846";
9847
9848        let program = Program::parse(initial_source).unwrap().0.unwrap();
9849
9850        let mut frontend = FrontendState::new();
9851
9852        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9853        let mock_ctx = ExecutorContext::new_mock(None).await;
9854        let version = Version(0);
9855
9856        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9857        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9858        let sketch_id = sketch_object.id;
9859        let sketch = expect_sketch(sketch_object);
9860        let line3_id = *sketch.segments.get(8).unwrap();
9861
9862        let (src_delta, scene_delta) = frontend
9863            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9864            .await
9865            .unwrap();
9866        assert_eq!(
9867            src_delta.text.as_str(),
9868            "\
9869sketch(on = XY) {
9870  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9871  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9872  parallel([line1, line2])
9873}
9874"
9875        );
9876
9877        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9878        let sketch = expect_sketch(sketch_object);
9879        assert_eq!(sketch.constraints.len(), 1);
9880
9881        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9882        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9883            panic!("Expected constraint object");
9884        };
9885        let Constraint::Parallel(parallel) = constraint else {
9886            panic!("Expected parallel constraint");
9887        };
9888        assert_eq!(parallel.lines.len(), 2);
9889
9890        ctx.close().await;
9891        mock_ctx.close().await;
9892    }
9893
9894    #[tokio::test(flavor = "multi_thread")]
9895    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
9896        let initial_source = "\
9897sketch(on = XY) {
9898  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9899  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9900  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9901  parallel([line1, line2, line3])
9902}
9903";
9904
9905        let program = Program::parse(initial_source).unwrap().0.unwrap();
9906
9907        let mut frontend = FrontendState::new();
9908
9909        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9910        let mock_ctx = ExecutorContext::new_mock(None).await;
9911        let version = Version(0);
9912
9913        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9914        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9915        let sketch_id = sketch_object.id;
9916        let sketch = expect_sketch(sketch_object);
9917        let line2_id = *sketch.segments.get(5).unwrap();
9918        let line3_id = *sketch.segments.get(8).unwrap();
9919
9920        let (src_delta, scene_delta) = frontend
9921            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9922            .await
9923            .unwrap();
9924        assert_eq!(
9925            src_delta.text.as_str(),
9926            "\
9927sketch(on = XY) {
9928  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9929}
9930"
9931        );
9932
9933        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9934        let sketch = expect_sketch(sketch_object);
9935        assert!(sketch.constraints.is_empty());
9936
9937        ctx.close().await;
9938        mock_ctx.close().await;
9939    }
9940
9941    #[tokio::test(flavor = "multi_thread")]
9942    async fn test_delete_line_line_coincident_constraint() {
9943        let initial_source = "\
9944sketch(on = XY) {
9945  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9946  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9947  coincident([line1, line2])
9948}
9949";
9950
9951        let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953        let mut frontend = FrontendState::new();
9954
9955        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9956        let mock_ctx = ExecutorContext::new_mock(None).await;
9957        let version = Version(0);
9958
9959        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9960        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961        let sketch_id = sketch_object.id;
9962        let sketch = expect_sketch(sketch_object);
9963
9964        let coincident_id = *sketch.constraints.first().unwrap();
9965
9966        let (src_delta, scene_delta) = frontend
9967            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9968            .await
9969            .unwrap();
9970        assert_eq!(
9971            src_delta.text.as_str(),
9972            "\
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}
9977"
9978        );
9979        assert_eq!(scene_delta.new_objects, vec![]);
9980        assert_eq!(scene_delta.new_graph.objects.len(), 8);
9981
9982        ctx.close().await;
9983        mock_ctx.close().await;
9984    }
9985
9986    #[tokio::test(flavor = "multi_thread")]
9987    async fn test_two_points_coincident() {
9988        let initial_source = "\
9989sketch(on = XY) {
9990  point1 = point(at = [var 1, var 2])
9991  point(at = [3, 4])
9992}
9993";
9994
9995        let program = Program::parse(initial_source).unwrap().0.unwrap();
9996
9997        let mut frontend = FrontendState::new();
9998
9999        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10000        let mock_ctx = ExecutorContext::new_mock(None).await;
10001        let version = Version(0);
10002
10003        frontend.hack_set_program(&ctx, program).await.unwrap();
10004        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10005        let sketch_id = sketch_object.id;
10006        let sketch = expect_sketch(sketch_object);
10007        let point0_id = *sketch.segments.first().unwrap();
10008        let point1_id = *sketch.segments.get(1).unwrap();
10009
10010        let constraint = Constraint::Coincident(Coincident {
10011            segments: vec![point0_id.into(), point1_id.into()],
10012        });
10013        let (src_delta, scene_delta) = frontend
10014            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10015            .await
10016            .unwrap();
10017        assert_eq!(
10018            src_delta.text.as_str(),
10019            "\
10020sketch(on = XY) {
10021  point1 = point(at = [var 3, var 4])
10022  point2 = point(at = [3, 4])
10023  coincident([point1, point2])
10024}
10025"
10026        );
10027        assert_eq!(
10028            scene_delta.new_graph.objects.len(),
10029            5,
10030            "{:#?}",
10031            scene_delta.new_graph.objects
10032        );
10033
10034        ctx.close().await;
10035        mock_ctx.close().await;
10036    }
10037
10038    #[tokio::test(flavor = "multi_thread")]
10039    async fn test_three_points_coincident() {
10040        let initial_source = "\
10041sketch(on = XY) {
10042  point1 = point(at = [var 1, var 2])
10043  point(at = [var 3, var 4])
10044  point(at = [var 5, var 6])
10045}
10046";
10047
10048        let program = Program::parse(initial_source).unwrap().0.unwrap();
10049
10050        let mut frontend = FrontendState::new();
10051
10052        let mock_ctx = ExecutorContext::new_mock(None).await;
10053        let version = Version(0);
10054
10055        frontend.program = program.clone();
10056        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10057        frontend.update_state_after_exec(outcome, true);
10058        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10059        let sketch_id = sketch_object.id;
10060        let sketch = expect_sketch(sketch_object);
10061        let segments = sketch
10062            .segments
10063            .iter()
10064            .take(3)
10065            .copied()
10066            .map(Into::into)
10067            .collect::<Vec<ConstraintSegment>>();
10068
10069        let constraint = Constraint::Coincident(Coincident {
10070            segments: segments.clone(),
10071        });
10072        let (src_delta, scene_delta) = frontend
10073            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10074            .await
10075            .unwrap();
10076        assert_eq!(
10077            src_delta.text.as_str(),
10078            "\
10079sketch(on = XY) {
10080  point1 = point(at = [var 3, var 4])
10081  point2 = point(at = [var 3, var 4])
10082  point3 = point(at = [var 3, var 4])
10083  coincident([point1, point2, point3])
10084}
10085"
10086        );
10087
10088        let constraint_object = scene_delta
10089            .new_graph
10090            .objects
10091            .iter()
10092            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10093            .unwrap();
10094
10095        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10096            panic!("expected a constraint object");
10097        };
10098
10099        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10100
10101        mock_ctx.close().await;
10102    }
10103
10104    #[tokio::test(flavor = "multi_thread")]
10105    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10106        let initial_source = "\
10107sketch(on = XY) {
10108  point1 = point(at = [var 1, var 2])
10109  point2 = point(at = [var 3, var 4])
10110  point3 = point(at = [var 5, var 6])
10111  coincident([point1, point2, point3])
10112}
10113";
10114
10115        let program = Program::parse(initial_source).unwrap().0.unwrap();
10116
10117        let mut frontend = FrontendState::new();
10118
10119        let ctx = ExecutorContext::new_mock(None).await;
10120        frontend.program = program.clone();
10121        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10122        frontend.update_state_after_exec(outcome, true);
10123
10124        let constraint_object = frontend
10125            .scene_graph
10126            .objects
10127            .iter()
10128            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10129            .unwrap();
10130        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10131            panic!("expected a constraint object");
10132        };
10133
10134        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10135        let sketch = expect_sketch(sketch_object);
10136        let expected_segments = sketch
10137            .segments
10138            .iter()
10139            .take(3)
10140            .copied()
10141            .map(Into::into)
10142            .collect::<Vec<ConstraintSegment>>();
10143
10144        assert_eq!(
10145            constraint,
10146            &Constraint::Coincident(Coincident {
10147                segments: expected_segments,
10148            })
10149        );
10150
10151        ctx.close().await;
10152    }
10153
10154    #[tokio::test(flavor = "multi_thread")]
10155    async fn test_point_origin_coincident_preserves_order() {
10156        let initial_source = "\
10157sketch(on = XY) {
10158  point(at = [var 1, var 2])
10159}
10160";
10161
10162        for (origin_first, expected_source) in [
10163            (
10164                true,
10165                "\
10166sketch(on = XY) {
10167  point1 = point(at = [var 0, var 0])
10168  coincident([ORIGIN, point1])
10169}
10170",
10171            ),
10172            (
10173                false,
10174                "\
10175sketch(on = XY) {
10176  point1 = point(at = [var 0, var 0])
10177  coincident([point1, ORIGIN])
10178}
10179",
10180            ),
10181        ] {
10182            let program = Program::parse(initial_source).unwrap().0.unwrap();
10183
10184            let mut frontend = FrontendState::new();
10185
10186            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10187            let mock_ctx = ExecutorContext::new_mock(None).await;
10188            let version = Version(0);
10189
10190            frontend.hack_set_program(&ctx, program).await.unwrap();
10191            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10192            let sketch_id = sketch_object.id;
10193            let sketch = expect_sketch(sketch_object);
10194            let point_id = *sketch.segments.first().unwrap();
10195
10196            let segments = if origin_first {
10197                vec![ConstraintSegment::ORIGIN, point_id.into()]
10198            } else {
10199                vec![point_id.into(), ConstraintSegment::ORIGIN]
10200            };
10201            let constraint = Constraint::Coincident(Coincident {
10202                segments: segments.clone(),
10203            });
10204            let (src_delta, scene_delta) = frontend
10205                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10206                .await
10207                .unwrap();
10208            assert_eq!(src_delta.text.as_str(), expected_source);
10209
10210            let constraint_object = scene_delta
10211                .new_graph
10212                .objects
10213                .iter()
10214                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10215                .unwrap();
10216
10217            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10218                panic!("expected a constraint object");
10219            };
10220
10221            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10222
10223            ctx.close().await;
10224            mock_ctx.close().await;
10225        }
10226    }
10227
10228    #[tokio::test(flavor = "multi_thread")]
10229    async fn test_coincident_of_line_end_points() {
10230        let initial_source = "\
10231sketch(on = XY) {
10232  line(start = [var 1, var 2], end = [var 3, var 4])
10233  line(start = [var 5, var 6], end = [var 7, var 8])
10234}
10235";
10236
10237        let program = Program::parse(initial_source).unwrap().0.unwrap();
10238
10239        let mut frontend = FrontendState::new();
10240
10241        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10242        let mock_ctx = ExecutorContext::new_mock(None).await;
10243        let version = Version(0);
10244
10245        frontend.hack_set_program(&ctx, program).await.unwrap();
10246        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10247        let sketch_id = sketch_object.id;
10248        let sketch = expect_sketch(sketch_object);
10249        let point0_id = *sketch.segments.get(1).unwrap();
10250        let point1_id = *sketch.segments.get(3).unwrap();
10251
10252        let constraint = Constraint::Coincident(Coincident {
10253            segments: vec![point0_id.into(), point1_id.into()],
10254        });
10255        let (src_delta, scene_delta) = frontend
10256            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10257            .await
10258            .unwrap();
10259        assert_eq!(
10260            src_delta.text.as_str(),
10261            "\
10262sketch(on = XY) {
10263  line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10264  line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10265  coincident([line1.end, line2.start])
10266}
10267"
10268        );
10269        assert_eq!(
10270            scene_delta.new_graph.objects.len(),
10271            9,
10272            "{:#?}",
10273            scene_delta.new_graph.objects
10274        );
10275
10276        ctx.close().await;
10277        mock_ctx.close().await;
10278    }
10279
10280    #[tokio::test(flavor = "multi_thread")]
10281    async fn test_coincident_of_line_point_and_circle_segment() {
10282        let initial_source = "\
10283sketch(on = XY) {
10284  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10285  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10286}
10287";
10288        let program = Program::parse(initial_source).unwrap().0.unwrap();
10289        let mut frontend = FrontendState::new();
10290
10291        let mock_ctx = ExecutorContext::new_mock(None).await;
10292        let version = Version(0);
10293
10294        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10295        frontend.program = program;
10296        frontend.update_state_after_exec(outcome, true);
10297        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10298        let sketch_id = sketch_object.id;
10299        let sketch = expect_sketch(sketch_object);
10300
10301        let circle_id = sketch
10302            .segments
10303            .iter()
10304            .copied()
10305            .find(|seg_id| {
10306                matches!(
10307                    &frontend.scene_graph.objects[seg_id.0].kind,
10308                    ObjectKind::Segment {
10309                        segment: Segment::Circle(_)
10310                    }
10311                )
10312            })
10313            .expect("Expected a circle segment in sketch");
10314        let line_id = frontend
10315            .scene_graph
10316            .objects
10317            .iter()
10318            .find_map(|obj| match &obj.kind {
10319                ObjectKind::Segment {
10320                    segment: Segment::Line(line),
10321                } if line.owner.is_none() => Some(obj.id),
10322                _ => None,
10323            })
10324            .expect("Expected a standalone line segment in scene graph");
10325
10326        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10327            ObjectKind::Segment {
10328                segment: Segment::Line(line),
10329            } => line.start,
10330            _ => panic!("Expected line segment object"),
10331        };
10332
10333        let constraint = Constraint::Coincident(Coincident {
10334            segments: vec![line_start_point_id.into(), circle_id.into()],
10335        });
10336        let (src_delta, _scene_delta) = frontend
10337            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10338            .await
10339            .unwrap();
10340        assert_eq!(
10341            src_delta.text.as_str(),
10342            "\
10343sketch(on = XY) {
10344  circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10345  line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10346  coincident([line1.start, circle1])
10347}
10348"
10349        );
10350
10351        mock_ctx.close().await;
10352    }
10353
10354    #[tokio::test(flavor = "multi_thread")]
10355    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10356        // Test that attempting an invalid coincident constraint (arc and line)
10357        // doesn't corrupt the state, allowing subsequent operations to work.
10358        // This test verifies the transactional fix in add_constraint that prevents
10359        // state corruption when invalid constraints are attempted.
10360        // Example: coincident constraint between an arc segment and a straight line segment
10361        // is geometrically invalid and should fail, but state should remain intact.
10362        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10363        let program = Program::empty();
10364
10365        let mut frontend = FrontendState::new();
10366        frontend.program = program;
10367
10368        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10369        let mock_ctx = ExecutorContext::new_mock(None).await;
10370        let version = Version(0);
10371
10372        let sketch_args = SketchCtor {
10373            on: Plane::Default(PlaneName::Xy),
10374        };
10375        let (_src_delta, _scene_delta, sketch_id) = frontend
10376            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10377            .await
10378            .unwrap();
10379
10380        // Add an arc segment
10381        let arc_ctor = ArcCtor {
10382            start: Point2d {
10383                x: Expr::Var(Number {
10384                    value: 0.0,
10385                    units: NumericSuffix::Mm,
10386                }),
10387                y: Expr::Var(Number {
10388                    value: 0.0,
10389                    units: NumericSuffix::Mm,
10390                }),
10391            },
10392            end: Point2d {
10393                x: Expr::Var(Number {
10394                    value: 10.0,
10395                    units: NumericSuffix::Mm,
10396                }),
10397                y: Expr::Var(Number {
10398                    value: 10.0,
10399                    units: NumericSuffix::Mm,
10400                }),
10401            },
10402            center: Point2d {
10403                x: Expr::Var(Number {
10404                    value: 10.0,
10405                    units: NumericSuffix::Mm,
10406                }),
10407                y: Expr::Var(Number {
10408                    value: 0.0,
10409                    units: NumericSuffix::Mm,
10410                }),
10411            },
10412            construction: None,
10413        };
10414        let (_src_delta, scene_delta) = frontend
10415            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10416            .await
10417            .unwrap();
10418        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10419        let arc_id = *scene_delta.new_objects.last().unwrap();
10420
10421        // Add a line segment
10422        let line_ctor = LineCtor {
10423            start: Point2d {
10424                x: Expr::Var(Number {
10425                    value: 20.0,
10426                    units: NumericSuffix::Mm,
10427                }),
10428                y: Expr::Var(Number {
10429                    value: 0.0,
10430                    units: NumericSuffix::Mm,
10431                }),
10432            },
10433            end: Point2d {
10434                x: Expr::Var(Number {
10435                    value: 30.0,
10436                    units: NumericSuffix::Mm,
10437                }),
10438                y: Expr::Var(Number {
10439                    value: 10.0,
10440                    units: NumericSuffix::Mm,
10441                }),
10442            },
10443            construction: None,
10444        };
10445        let (_src_delta, scene_delta) = frontend
10446            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10447            .await
10448            .unwrap();
10449        // The line is the last object in new_objects (after the 2 points: start, end)
10450        let line_id = *scene_delta.new_objects.last().unwrap();
10451
10452        // Attempt to add an invalid coincident constraint between arc and line
10453        // This should fail during execution, but state should remain intact
10454        let constraint = Constraint::Coincident(Coincident {
10455            segments: vec![arc_id.into(), line_id.into()],
10456        });
10457        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10458
10459        // The constraint addition should fail (invalid constraint)
10460        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10461
10462        // Verify state is not corrupted by checking that we can still access the scene graph
10463        // and that the original segments are still present with their source ranges
10464        let sketch_object_after =
10465            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10466        let sketch_after = expect_sketch(sketch_object_after);
10467
10468        // Verify both segments are still in the sketch
10469        assert!(
10470            sketch_after.segments.contains(&arc_id),
10471            "Arc segment should still exist after failed constraint"
10472        );
10473        assert!(
10474            sketch_after.segments.contains(&line_id),
10475            "Line segment should still exist after failed constraint"
10476        );
10477
10478        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10479        let arc_obj = frontend
10480            .scene_graph
10481            .objects
10482            .get(arc_id.0)
10483            .expect("Arc object should still be accessible");
10484        let line_obj = frontend
10485            .scene_graph
10486            .objects
10487            .get(line_id.0)
10488            .expect("Line object should still be accessible");
10489
10490        // Verify source ranges are still valid (not corrupted)
10491        // Just verify that the objects are still accessible and have the expected types
10492        match &arc_obj.kind {
10493            ObjectKind::Segment {
10494                segment: Segment::Arc(_),
10495            } => {}
10496            _ => panic!("Arc object should still be an arc segment"),
10497        }
10498        match &line_obj.kind {
10499            ObjectKind::Segment {
10500                segment: Segment::Line(_),
10501            } => {}
10502            _ => panic!("Line object should still be a line segment"),
10503        }
10504
10505        ctx.close().await;
10506        mock_ctx.close().await;
10507    }
10508
10509    #[tokio::test(flavor = "multi_thread")]
10510    async fn test_distance_two_points() {
10511        let initial_source = "\
10512sketch(on = XY) {
10513  point(at = [var 1, var 2])
10514  point(at = [var 3, var 4])
10515}
10516";
10517
10518        let program = Program::parse(initial_source).unwrap().0.unwrap();
10519
10520        let mut frontend = FrontendState::new();
10521
10522        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10523        let mock_ctx = ExecutorContext::new_mock(None).await;
10524        let version = Version(0);
10525
10526        frontend.hack_set_program(&ctx, program).await.unwrap();
10527        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10528        let sketch_id = sketch_object.id;
10529        let sketch = expect_sketch(sketch_object);
10530        let point0_id = *sketch.segments.first().unwrap();
10531        let point1_id = *sketch.segments.get(1).unwrap();
10532
10533        let constraint = Constraint::Distance(Distance {
10534            points: vec![point0_id.into(), point1_id.into()],
10535            distance: Number {
10536                value: 2.0,
10537                units: NumericSuffix::Mm,
10538            },
10539            label_position: None,
10540            source: Default::default(),
10541        });
10542        let (src_delta, scene_delta) = frontend
10543            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10544            .await
10545            .unwrap();
10546        assert_eq!(
10547            src_delta.text.as_str(),
10548            // The lack indentation is a formatter bug.
10549            "\
10550sketch(on = XY) {
10551  point1 = point(at = [var 1.29, var 2.29])
10552  point2 = point(at = [var 2.71, var 3.71])
10553  distance([point1, point2]) == 2mm
10554}
10555"
10556        );
10557        assert_eq!(
10558            scene_delta.new_graph.objects.len(),
10559            5,
10560            "{:#?}",
10561            scene_delta.new_graph.objects
10562        );
10563
10564        ctx.close().await;
10565        mock_ctx.close().await;
10566    }
10567
10568    #[tokio::test(flavor = "multi_thread")]
10569    async fn test_distance_two_points_with_label() {
10570        let initial_source = "\
10571sketch(on = XY) {
10572  point(at = [var 1, var 2])
10573  point(at = [var 3, var 4])
10574}
10575";
10576
10577        let program = Program::parse(initial_source).unwrap().0.unwrap();
10578
10579        let mut frontend = FrontendState::new();
10580
10581        let mock_ctx = ExecutorContext::new_mock(None).await;
10582        let version = Version(0);
10583
10584        frontend.program = program.clone();
10585        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10586        frontend.update_state_after_exec(outcome, true);
10587        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10588        let sketch_id = sketch_object.id;
10589        let sketch = expect_sketch(sketch_object);
10590        let point0_id = *sketch.segments.first().unwrap();
10591        let point1_id = *sketch.segments.get(1).unwrap();
10592
10593        let label_position = Point2d {
10594            x: Number {
10595                value: 10.0,
10596                units: NumericSuffix::Mm,
10597            },
10598            y: Number {
10599                value: 11.0,
10600                units: NumericSuffix::Mm,
10601            },
10602        };
10603        let constraint = Constraint::Distance(Distance {
10604            points: vec![point0_id.into(), point1_id.into()],
10605            distance: Number {
10606                value: 2.0,
10607                units: NumericSuffix::Mm,
10608            },
10609            label_position: Some(label_position.clone()),
10610            source: Default::default(),
10611        });
10612        let (src_delta, scene_delta) = frontend
10613            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10614            .await
10615            .unwrap();
10616        assert_eq!(
10617            src_delta.text.as_str(),
10618            "\
10619sketch(on = XY) {
10620  point1 = point(at = [var 1.29, var 2.29])
10621  point2 = point(at = [var 2.71, var 3.71])
10622  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10623}
10624"
10625        );
10626
10627        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10628        let sketch = expect_sketch(sketch_object);
10629        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10630        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10631            panic!("Expected constraint object");
10632        };
10633        let Constraint::Distance(distance) = constraint else {
10634            panic!("Expected distance constraint");
10635        };
10636        assert_eq!(distance.label_position, Some(label_position));
10637
10638        mock_ctx.close().await;
10639    }
10640
10641    #[tokio::test(flavor = "multi_thread")]
10642    async fn test_edit_distance_constraint_label_position() {
10643        let initial_source = "\
10644sketch(on = XY) {
10645  point(at = [var 1, var 2])
10646  point(at = [var 3, var 2])
10647}
10648";
10649
10650        let program = Program::parse(initial_source).unwrap().0.unwrap();
10651
10652        let mut frontend = FrontendState::new();
10653
10654        let mock_ctx = ExecutorContext::new_mock(None).await;
10655        let version = Version(0);
10656
10657        frontend.program = program.clone();
10658        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10659        frontend.update_state_after_exec(outcome, true);
10660        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10661        let sketch_id = sketch_object.id;
10662        let sketch = expect_sketch(sketch_object);
10663        let point0_id = *sketch.segments.first().unwrap();
10664        let point1_id = *sketch.segments.get(1).unwrap();
10665
10666        let constraint = Constraint::Distance(Distance {
10667            points: vec![point0_id.into(), point1_id.into()],
10668            distance: Number {
10669                value: 2.0,
10670                units: NumericSuffix::Mm,
10671            },
10672            label_position: None,
10673            source: Default::default(),
10674        });
10675        let (_, scene_delta) = frontend
10676            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10677            .await
10678            .unwrap();
10679        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10680        let sketch = expect_sketch(sketch_object);
10681        let constraint_id = sketch.constraints[0];
10682        let label_position = Point2d {
10683            x: Number {
10684                value: 10.0,
10685                units: NumericSuffix::Mm,
10686            },
10687            y: Number {
10688                value: 11.0,
10689                units: NumericSuffix::Mm,
10690            },
10691        };
10692
10693        let (src_delta, scene_delta) = frontend
10694            .edit_distance_constraint_label_position(
10695                &mock_ctx,
10696                version,
10697                sketch_id,
10698                constraint_id,
10699                label_position.clone(),
10700                vec![],
10701            )
10702            .await
10703            .unwrap();
10704        assert_eq!(
10705            src_delta.text.as_str(),
10706            "\
10707sketch(on = XY) {
10708  point1 = point(at = [var 1, var 2])
10709  point2 = point(at = [var 3, var 2])
10710  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10711}
10712"
10713        );
10714
10715        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10716        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10717            panic!("Expected constraint object");
10718        };
10719        let Constraint::Distance(distance) = constraint else {
10720            panic!("Expected distance constraint");
10721        };
10722        assert_eq!(distance.label_position, Some(label_position));
10723
10724        mock_ctx.close().await;
10725    }
10726
10727    #[tokio::test(flavor = "multi_thread")]
10728    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10729        let initial_source = "\
10730sketch(on = XY) {
10731  point1 = point(at = [var 0mm, var 0mm])
10732  point2 = point(at = [var 10mm, var 0mm])
10733  distance([point1, point2]) == 5mm
10734}
10735";
10736
10737        let program = Program::parse(initial_source).unwrap().0.unwrap();
10738        let mut frontend = FrontendState::new();
10739        let mock_ctx = ExecutorContext::new_mock(None).await;
10740        let version = Version(0);
10741
10742        frontend.program = program.clone();
10743        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10744        frontend.update_state_after_exec(outcome, true);
10745        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10746        let sketch_id = sketch_object.id;
10747        let sketch = expect_sketch(sketch_object);
10748        let point0_id = sketch.segments[0];
10749        let point1_id = sketch.segments[1];
10750        let constraint_id = sketch.constraints[0];
10751
10752        let edited_segments = vec![ExistingSegmentCtor {
10753            id: point0_id,
10754            ctor: SegmentCtor::Point(PointCtor {
10755                position: Point2d {
10756                    x: Expr::Var(Number {
10757                        value: 2.0,
10758                        units: NumericSuffix::Mm,
10759                    }),
10760                    y: Expr::Var(Number {
10761                        value: 1.0,
10762                        units: NumericSuffix::Mm,
10763                    }),
10764                },
10765            }),
10766        }];
10767        let (_, scene_delta) = frontend
10768            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10769            .await
10770            .unwrap();
10771        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10772        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10773
10774        let label_position = Point2d {
10775            x: Number {
10776                value: 3.0,
10777                units: NumericSuffix::Mm,
10778            },
10779            y: Number {
10780                value: 4.0,
10781                units: NumericSuffix::Mm,
10782            },
10783        };
10784        let (_, scene_delta) = frontend
10785            .edit_distance_constraint_label_position(
10786                &mock_ctx,
10787                version,
10788                sketch_id,
10789                constraint_id,
10790                label_position,
10791                vec![point0_id],
10792            )
10793            .await
10794            .unwrap();
10795
10796        assert_point_position_close(
10797            point_position(&scene_delta.new_graph, point0_id),
10798            point0_after_segment_edit,
10799        );
10800        assert_point_position_close(
10801            point_position(&scene_delta.new_graph, point1_id),
10802            point1_after_segment_edit,
10803        );
10804
10805        mock_ctx.close().await;
10806    }
10807
10808    #[tokio::test(flavor = "multi_thread")]
10809    async fn test_distance_point_line() {
10810        let initial_source = "\
10811sketch(on = XY) {
10812  point(at = [var 0, var 5])
10813  line(start = [var 0, var 0], end = [var 10, var 0])
10814}
10815";
10816
10817        let program = Program::parse(initial_source).unwrap().0.unwrap();
10818
10819        let mut frontend = FrontendState::new();
10820
10821        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10822        let mock_ctx = ExecutorContext::new_mock(None).await;
10823        let version = Version(0);
10824
10825        frontend.hack_set_program(&ctx, program).await.unwrap();
10826        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10827        let sketch_id = sketch_object.id;
10828        let sketch = expect_sketch(sketch_object);
10829        let point_id = *sketch.segments.first().unwrap();
10830        let line_id = *sketch
10831            .segments
10832            .iter()
10833            .find(|segment_id| {
10834                matches!(
10835                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10836                    Some(ObjectKind::Segment {
10837                        segment: Segment::Line(_)
10838                    })
10839                )
10840            })
10841            .unwrap();
10842
10843        let label_position = Point2d {
10844            x: Number {
10845                value: 10.0,
10846                units: NumericSuffix::Mm,
10847            },
10848            y: Number {
10849                value: 11.0,
10850                units: NumericSuffix::Mm,
10851            },
10852        };
10853        let constraint = Constraint::Distance(Distance {
10854            points: vec![point_id.into(), line_id.into()],
10855            distance: Number {
10856                value: 5.0,
10857                units: NumericSuffix::Mm,
10858            },
10859            label_position: Some(label_position.clone()),
10860            source: Default::default(),
10861        });
10862        let (src_delta, scene_delta) = frontend
10863            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10864            .await
10865            .unwrap();
10866        assert_eq!(
10867            src_delta.text.as_str(),
10868            "\
10869sketch(on = XY) {
10870  point1 = point(at = [var 0, var 5])
10871  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10872  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
10873}
10874"
10875        );
10876        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10877        let sketch = expect_sketch(sketch_object);
10878        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10879        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10880            panic!("Expected constraint object");
10881        };
10882        let Constraint::Distance(distance) = constraint else {
10883            panic!("Expected distance constraint");
10884        };
10885        assert_eq!(distance.label_position, Some(label_position));
10886
10887        ctx.close().await;
10888        mock_ctx.close().await;
10889    }
10890
10891    #[tokio::test(flavor = "multi_thread")]
10892    async fn test_distance_point_arc() {
10893        let initial_source = "\
10894sketch(on = XY) {
10895  point(at = [var 0, var 8])
10896  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10897}
10898";
10899
10900        let program = Program::parse(initial_source).unwrap().0.unwrap();
10901
10902        let mut frontend = FrontendState::new();
10903
10904        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10905        let mock_ctx = ExecutorContext::new_mock(None).await;
10906        let version = Version(0);
10907
10908        frontend.hack_set_program(&ctx, program).await.unwrap();
10909        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10910        let sketch_id = sketch_object.id;
10911        let sketch = expect_sketch(sketch_object);
10912        let point_id = *sketch.segments.first().unwrap();
10913        let arc_id = *sketch
10914            .segments
10915            .iter()
10916            .find(|segment_id| {
10917                matches!(
10918                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10919                    Some(ObjectKind::Segment {
10920                        segment: Segment::Arc(_)
10921                    })
10922                )
10923            })
10924            .unwrap();
10925
10926        let constraint = Constraint::Distance(Distance {
10927            points: vec![point_id.into(), arc_id.into()],
10928            distance: Number {
10929                value: 3.0,
10930                units: NumericSuffix::Mm,
10931            },
10932            label_position: None,
10933            source: Default::default(),
10934        });
10935        let (src_delta, _scene_delta) = frontend
10936            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10937            .await
10938            .unwrap();
10939        assert_eq!(
10940            src_delta.text.as_str(),
10941            "\
10942sketch(on = XY) {
10943  point1 = point(at = [var 0, var 8])
10944  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10945  distance([point1, arc1]) == 3mm
10946}
10947"
10948        );
10949
10950        ctx.close().await;
10951        mock_ctx.close().await;
10952    }
10953
10954    #[tokio::test(flavor = "multi_thread")]
10955    async fn test_distance_arc_origin() {
10956        let initial_source = "\
10957sketch001 = sketch(on = XY) {
10958  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10959}
10960";
10961
10962        let program = Program::parse(initial_source).unwrap().0.unwrap();
10963
10964        let mut frontend = FrontendState::new();
10965
10966        let mock_ctx = ExecutorContext::new_mock(None).await;
10967        let version = Version(0);
10968
10969        frontend.program = program.clone();
10970        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10971        frontend.update_state_after_exec(outcome, true);
10972        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10973        let sketch_id = sketch_object.id;
10974        let sketch = expect_sketch(sketch_object);
10975        let arc_id = *sketch
10976            .segments
10977            .iter()
10978            .find(|segment_id| {
10979                matches!(
10980                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10981                    Some(ObjectKind::Segment {
10982                        segment: Segment::Arc(_)
10983                    })
10984                )
10985            })
10986            .unwrap();
10987
10988        let constraint = Constraint::Distance(Distance {
10989            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10990            distance: Number {
10991                value: 3.0,
10992                units: NumericSuffix::Mm,
10993            },
10994            label_position: None,
10995            source: Default::default(),
10996        });
10997        let (src_delta, _scene_delta) = frontend
10998            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10999            .await
11000            .unwrap();
11001        assert_eq!(
11002            src_delta.text.as_str(),
11003            "\
11004sketch001 = sketch(on = XY) {
11005  arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11006  distance([arc1, ORIGIN]) == 3mm
11007}
11008"
11009        );
11010
11011        mock_ctx.close().await;
11012    }
11013
11014    #[tokio::test(flavor = "multi_thread")]
11015    async fn test_distance_line_origin() {
11016        let initial_source = "\
11017sketch(on = XY) {
11018  line(start = [var 5, var 0], end = [var 5, var 10])
11019}
11020";
11021
11022        let program = Program::parse(initial_source).unwrap().0.unwrap();
11023
11024        let mut frontend = FrontendState::new();
11025
11026        let mock_ctx = ExecutorContext::new_mock(None).await;
11027        let version = Version(0);
11028
11029        frontend.program = program.clone();
11030        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11031        frontend.update_state_after_exec(outcome, true);
11032        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11033        let sketch_id = sketch_object.id;
11034        let sketch = expect_sketch(sketch_object);
11035        let line_id = *sketch
11036            .segments
11037            .iter()
11038            .find(|segment_id| {
11039                matches!(
11040                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11041                    Some(ObjectKind::Segment {
11042                        segment: Segment::Line(_)
11043                    })
11044                )
11045            })
11046            .unwrap();
11047
11048        let constraint = Constraint::Distance(Distance {
11049            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11050            distance: Number {
11051                value: 5.0,
11052                units: NumericSuffix::Mm,
11053            },
11054            label_position: None,
11055            source: Default::default(),
11056        });
11057        let (src_delta, _scene_delta) = frontend
11058            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11059            .await
11060            .unwrap();
11061        assert_eq!(
11062            src_delta.text.as_str(),
11063            "\
11064sketch(on = XY) {
11065  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11066  distance([ORIGIN, line1]) == 5mm
11067}
11068"
11069        );
11070
11071        mock_ctx.close().await;
11072    }
11073
11074    #[tokio::test(flavor = "multi_thread")]
11075    async fn test_distance_line_circle() {
11076        let initial_source = "\
11077sketch(on = XY) {
11078  line(start = [var -10, var 8], end = [var 10, var 8])
11079  circle(start = [var 5, var 0], center = [var 0, var 0])
11080}
11081";
11082
11083        let program = Program::parse(initial_source).unwrap().0.unwrap();
11084
11085        let mut frontend = FrontendState::new();
11086
11087        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11088        let mock_ctx = ExecutorContext::new_mock(None).await;
11089        let version = Version(0);
11090
11091        frontend.hack_set_program(&ctx, program).await.unwrap();
11092        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11093        let sketch_id = sketch_object.id;
11094        let sketch = expect_sketch(sketch_object);
11095        let line_id = *sketch
11096            .segments
11097            .iter()
11098            .find(|segment_id| {
11099                matches!(
11100                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11101                    Some(ObjectKind::Segment {
11102                        segment: Segment::Line(_)
11103                    })
11104                )
11105            })
11106            .unwrap();
11107        let circle_id = *sketch
11108            .segments
11109            .iter()
11110            .find(|segment_id| {
11111                matches!(
11112                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11113                    Some(ObjectKind::Segment {
11114                        segment: Segment::Circle(_)
11115                    })
11116                )
11117            })
11118            .unwrap();
11119
11120        let constraint = Constraint::Distance(Distance {
11121            points: vec![line_id.into(), circle_id.into()],
11122            distance: Number {
11123                value: 3.0,
11124                units: NumericSuffix::Mm,
11125            },
11126            label_position: None,
11127            source: Default::default(),
11128        });
11129        let (src_delta, _scene_delta) = frontend
11130            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11131            .await
11132            .unwrap();
11133        assert_eq!(
11134            src_delta.text.as_str(),
11135            "\
11136sketch(on = XY) {
11137  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11138  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11139  distance([line1, circle1]) == 3mm
11140}
11141"
11142        );
11143
11144        ctx.close().await;
11145        mock_ctx.close().await;
11146    }
11147
11148    #[tokio::test(flavor = "multi_thread")]
11149    async fn test_distance_circle_arc() {
11150        let initial_source = "\
11151sketch(on = XY) {
11152  circle(start = [var 5, var 0], center = [var 0, var 0])
11153  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11154}
11155";
11156
11157        let program = Program::parse(initial_source).unwrap().0.unwrap();
11158
11159        let mut frontend = FrontendState::new();
11160
11161        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11162        let mock_ctx = ExecutorContext::new_mock(None).await;
11163        let version = Version(0);
11164
11165        frontend.hack_set_program(&ctx, program).await.unwrap();
11166        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11167        let sketch_id = sketch_object.id;
11168        let sketch = expect_sketch(sketch_object);
11169        let circle_id = *sketch
11170            .segments
11171            .iter()
11172            .find(|segment_id| {
11173                matches!(
11174                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11175                    Some(ObjectKind::Segment {
11176                        segment: Segment::Circle(_)
11177                    })
11178                )
11179            })
11180            .unwrap();
11181        let arc_id = *sketch
11182            .segments
11183            .iter()
11184            .find(|segment_id| {
11185                matches!(
11186                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11187                    Some(ObjectKind::Segment {
11188                        segment: Segment::Arc(_)
11189                    })
11190                )
11191            })
11192            .unwrap();
11193
11194        let constraint = Constraint::Distance(Distance {
11195            points: vec![circle_id.into(), arc_id.into()],
11196            distance: Number {
11197                value: 3.0,
11198                units: NumericSuffix::Mm,
11199            },
11200            label_position: None,
11201            source: Default::default(),
11202        });
11203        let (src_delta, _scene_delta) = frontend
11204            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11205            .await
11206            .unwrap();
11207        assert_eq!(
11208            src_delta.text.as_str(),
11209            "\
11210sketch(on = XY) {
11211  circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11212  arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11213  distance([circle1, arc1]) == 3mm
11214}
11215"
11216        );
11217
11218        ctx.close().await;
11219        mock_ctx.close().await;
11220    }
11221
11222    #[tokio::test(flavor = "multi_thread")]
11223    async fn test_distance_parallel_lines() {
11224        let initial_source = "\
11225sketch(on = XY) {
11226  line(start = [var 0, var 0], end = [var 10, var 0])
11227  line(start = [var 0, var 5], end = [var 10, var 5])
11228}
11229";
11230
11231        let program = Program::parse(initial_source).unwrap().0.unwrap();
11232
11233        let mut frontend = FrontendState::new();
11234
11235        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11236        let mock_ctx = ExecutorContext::new_mock(None).await;
11237        let version = Version(0);
11238
11239        frontend.hack_set_program(&ctx, program).await.unwrap();
11240        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11241        let sketch_id = sketch_object.id;
11242        let sketch = expect_sketch(sketch_object);
11243        let line_ids = sketch
11244            .segments
11245            .iter()
11246            .copied()
11247            .filter(|segment_id| {
11248                matches!(
11249                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11250                    Some(ObjectKind::Segment {
11251                        segment: Segment::Line(_)
11252                    })
11253                )
11254            })
11255            .collect::<Vec<_>>();
11256
11257        let constraint = Constraint::Distance(Distance {
11258            points: vec![line_ids[0].into(), line_ids[1].into()],
11259            distance: Number {
11260                value: 5.0,
11261                units: NumericSuffix::Mm,
11262            },
11263            label_position: None,
11264            source: Default::default(),
11265        });
11266        let (src_delta, _scene_delta) = frontend
11267            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11268            .await
11269            .unwrap();
11270        assert_eq!(
11271            src_delta.text.as_str(),
11272            "\
11273sketch(on = XY) {
11274  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11275  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11276  distance([line1, line2]) == 5mm
11277}
11278"
11279        );
11280
11281        ctx.close().await;
11282        mock_ctx.close().await;
11283    }
11284
11285    #[tokio::test(flavor = "multi_thread")]
11286    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11287        let initial_source = "\
11288sketch(on = XY) {
11289  line(start = [var 0, var 0], end = [var 10, var 0])
11290  line(start = [var 0, var 0], end = [var 0, var 10])
11291}
11292";
11293
11294        let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296        let mut frontend = FrontendState::new();
11297
11298        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11299        let mock_ctx = ExecutorContext::new_mock(None).await;
11300        let version = Version(0);
11301
11302        frontend.hack_set_program(&ctx, program).await.unwrap();
11303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11304        let sketch_id = sketch_object.id;
11305        let sketch = expect_sketch(sketch_object);
11306        let line_ids = sketch
11307            .segments
11308            .iter()
11309            .copied()
11310            .filter(|segment_id| {
11311                matches!(
11312                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11313                    Some(ObjectKind::Segment {
11314                        segment: Segment::Line(_)
11315                    })
11316                )
11317            })
11318            .collect::<Vec<_>>();
11319
11320        let constraint = Constraint::Distance(Distance {
11321            points: vec![line_ids[0].into(), line_ids[1].into()],
11322            distance: Number {
11323                value: 5.0,
11324                units: NumericSuffix::Mm,
11325            },
11326            label_position: None,
11327            source: Default::default(),
11328        });
11329        let (src_delta, _scene_delta) = frontend
11330            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11331            .await
11332            .unwrap();
11333        assert_eq!(
11334            src_delta.text.as_str(),
11335            "\
11336sketch(on = XY) {
11337  line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11338  line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11339  distance([line1, line2]) == 5mm
11340}
11341"
11342        );
11343
11344        ctx.close().await;
11345        mock_ctx.close().await;
11346    }
11347
11348    #[tokio::test(flavor = "multi_thread")]
11349    async fn test_horizontal_distance_two_points() {
11350        let initial_source = "\
11351sketch(on = XY) {
11352  point(at = [var 1, var 2])
11353  point(at = [var 3, var 4])
11354}
11355";
11356
11357        let program = Program::parse(initial_source).unwrap().0.unwrap();
11358
11359        let mut frontend = FrontendState::new();
11360
11361        let mock_ctx = ExecutorContext::new_mock(None).await;
11362        let version = Version(0);
11363
11364        frontend.program = program.clone();
11365        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11366        frontend.update_state_after_exec(outcome, true);
11367        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11368        let sketch_id = sketch_object.id;
11369        let sketch = expect_sketch(sketch_object);
11370        let point0_id = *sketch.segments.first().unwrap();
11371        let point1_id = *sketch.segments.get(1).unwrap();
11372        let label_position = Point2d {
11373            x: Number {
11374                value: 10.0,
11375                units: NumericSuffix::Mm,
11376            },
11377            y: Number {
11378                value: 11.0,
11379                units: NumericSuffix::Mm,
11380            },
11381        };
11382
11383        let constraint = Constraint::HorizontalDistance(Distance {
11384            points: vec![point0_id.into(), point1_id.into()],
11385            distance: Number {
11386                value: 2.0,
11387                units: NumericSuffix::Mm,
11388            },
11389            label_position: Some(label_position.clone()),
11390            source: Default::default(),
11391        });
11392        let (src_delta, scene_delta) = frontend
11393            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11394            .await
11395            .unwrap();
11396        assert_eq!(
11397            src_delta.text.as_str(),
11398            // The lack indentation is a formatter bug.
11399            "\
11400sketch(on = XY) {
11401  point1 = point(at = [var 1, var 2])
11402  point2 = point(at = [var 3, var 4])
11403  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11404}
11405"
11406        );
11407        assert_eq!(
11408            scene_delta.new_graph.objects.len(),
11409            5,
11410            "{:#?}",
11411            scene_delta.new_graph.objects
11412        );
11413        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11414        let sketch = expect_sketch(sketch_object);
11415        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11416        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11417            panic!("Expected constraint object");
11418        };
11419        let Constraint::HorizontalDistance(distance) = constraint else {
11420            panic!("Expected horizontal distance constraint");
11421        };
11422        assert_eq!(distance.label_position, Some(label_position));
11423
11424        mock_ctx.close().await;
11425    }
11426
11427    #[tokio::test(flavor = "multi_thread")]
11428    async fn test_radius_single_arc_segment() {
11429        let initial_source = "\
11430sketch(on = XY) {
11431  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11432}
11433";
11434
11435        let program = Program::parse(initial_source).unwrap().0.unwrap();
11436
11437        let mut frontend = FrontendState::new();
11438
11439        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11440        let mock_ctx = ExecutorContext::new_mock(None).await;
11441        let version = Version(0);
11442
11443        frontend.hack_set_program(&ctx, program).await.unwrap();
11444        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11445        let sketch_id = sketch_object.id;
11446        let sketch = expect_sketch(sketch_object);
11447        // Find the arc segment (not the points)
11448        let arc_id = sketch
11449            .segments
11450            .iter()
11451            .find(|&seg_id| {
11452                let obj = frontend.scene_graph.objects.get(seg_id.0);
11453                matches!(
11454                    obj.map(|o| &o.kind),
11455                    Some(ObjectKind::Segment {
11456                        segment: Segment::Arc(_)
11457                    })
11458                )
11459            })
11460            .unwrap();
11461
11462        let constraint = Constraint::Radius(Radius {
11463            arc: *arc_id,
11464            radius: Number {
11465                value: 5.0,
11466                units: NumericSuffix::Mm,
11467            },
11468            label_position: None,
11469            source: Default::default(),
11470        });
11471        let (src_delta, scene_delta) = frontend
11472            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11473            .await
11474            .unwrap();
11475        assert_eq!(
11476            src_delta.text.as_str(),
11477            // The lack indentation is a formatter bug.
11478            "\
11479sketch(on = XY) {
11480  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11481  radius(arc1) == 5mm
11482}
11483"
11484        );
11485        assert_eq!(
11486            scene_delta.new_graph.objects.len(),
11487            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11488            "{:#?}",
11489            scene_delta.new_graph.objects
11490        );
11491
11492        ctx.close().await;
11493        mock_ctx.close().await;
11494    }
11495
11496    #[tokio::test(flavor = "multi_thread")]
11497    async fn test_radius_single_arc_segment_with_label_position() {
11498        let initial_source = "\
11499sketch(on = XY) {
11500  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11501}
11502";
11503
11504        let program = Program::parse(initial_source).unwrap().0.unwrap();
11505        let mut frontend = FrontendState::new();
11506        let mock_ctx = ExecutorContext::new_mock(None).await;
11507        let version = Version(0);
11508
11509        frontend.program = program.clone();
11510        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11511        frontend.update_state_after_exec(outcome, true);
11512        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11513        let sketch_id = sketch_object.id;
11514        let sketch = expect_sketch(sketch_object);
11515        let arc_id = sketch
11516            .segments
11517            .iter()
11518            .find(|&seg_id| {
11519                let obj = frontend.scene_graph.objects.get(seg_id.0);
11520                matches!(
11521                    obj.map(|o| &o.kind),
11522                    Some(ObjectKind::Segment {
11523                        segment: Segment::Arc(_)
11524                    })
11525                )
11526            })
11527            .unwrap();
11528
11529        let label_position = Point2d {
11530            x: Number {
11531                value: 10.0,
11532                units: NumericSuffix::Mm,
11533            },
11534            y: Number {
11535                value: 11.0,
11536                units: NumericSuffix::Mm,
11537            },
11538        };
11539        let constraint = Constraint::Radius(Radius {
11540            arc: *arc_id,
11541            radius: Number {
11542                value: 5.0,
11543                units: NumericSuffix::Mm,
11544            },
11545            label_position: Some(label_position.clone()),
11546            source: Default::default(),
11547        });
11548        let (src_delta, scene_delta) = frontend
11549            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11550            .await
11551            .unwrap();
11552        assert_eq!(
11553            src_delta.text.as_str(),
11554            "\
11555sketch(on = XY) {
11556  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11557  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11558}
11559"
11560        );
11561
11562        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11563        let sketch = expect_sketch(sketch_object);
11564        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11565        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11566            panic!("Expected constraint object");
11567        };
11568        let Constraint::Radius(radius) = constraint else {
11569            panic!("Expected radius constraint");
11570        };
11571        assert_eq!(radius.label_position, Some(label_position));
11572
11573        mock_ctx.close().await;
11574    }
11575
11576    #[tokio::test(flavor = "multi_thread")]
11577    async fn test_edit_radius_constraint_label_position() {
11578        let initial_source = "\
11579sketch(on = XY) {
11580  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11581  radius(arc1) == 5mm
11582}
11583";
11584
11585        let program = Program::parse(initial_source).unwrap().0.unwrap();
11586        let mut frontend = FrontendState::new();
11587        let mock_ctx = ExecutorContext::new_mock(None).await;
11588        let version = Version(0);
11589
11590        frontend.program = program.clone();
11591        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11592        frontend.update_state_after_exec(outcome, true);
11593        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11594        let sketch_id = sketch_object.id;
11595        let sketch = expect_sketch(sketch_object);
11596        let constraint_id = sketch.constraints[0];
11597        let label_position = Point2d {
11598            x: Number {
11599                value: 10.0,
11600                units: NumericSuffix::Mm,
11601            },
11602            y: Number {
11603                value: 11.0,
11604                units: NumericSuffix::Mm,
11605            },
11606        };
11607
11608        let (src_delta, scene_delta) = frontend
11609            .edit_distance_constraint_label_position(
11610                &mock_ctx,
11611                version,
11612                sketch_id,
11613                constraint_id,
11614                label_position.clone(),
11615                vec![],
11616            )
11617            .await
11618            .unwrap();
11619        assert_eq!(
11620            src_delta.text.as_str(),
11621            "\
11622sketch(on = XY) {
11623  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11624  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11625}
11626"
11627        );
11628
11629        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11630        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11631            panic!("Expected constraint object");
11632        };
11633        let Constraint::Radius(radius) = constraint else {
11634            panic!("Expected radius constraint");
11635        };
11636        assert_eq!(radius.label_position, Some(label_position));
11637
11638        mock_ctx.close().await;
11639    }
11640
11641    #[tokio::test(flavor = "multi_thread")]
11642    async fn test_vertical_distance_two_points() {
11643        let initial_source = "\
11644sketch(on = XY) {
11645  point(at = [var 1, var 2])
11646  point(at = [var 3, var 4])
11647}
11648";
11649
11650        let program = Program::parse(initial_source).unwrap().0.unwrap();
11651
11652        let mut frontend = FrontendState::new();
11653
11654        let mock_ctx = ExecutorContext::new_mock(None).await;
11655        let version = Version(0);
11656
11657        frontend.program = program.clone();
11658        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11659        frontend.update_state_after_exec(outcome, true);
11660        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11661        let sketch_id = sketch_object.id;
11662        let sketch = expect_sketch(sketch_object);
11663        let point0_id = *sketch.segments.first().unwrap();
11664        let point1_id = *sketch.segments.get(1).unwrap();
11665        let label_position = Point2d {
11666            x: Number {
11667                value: 10.0,
11668                units: NumericSuffix::Mm,
11669            },
11670            y: Number {
11671                value: 11.0,
11672                units: NumericSuffix::Mm,
11673            },
11674        };
11675
11676        let constraint = Constraint::VerticalDistance(Distance {
11677            points: vec![point0_id.into(), point1_id.into()],
11678            distance: Number {
11679                value: 2.0,
11680                units: NumericSuffix::Mm,
11681            },
11682            label_position: Some(label_position.clone()),
11683            source: Default::default(),
11684        });
11685        let (src_delta, scene_delta) = frontend
11686            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11687            .await
11688            .unwrap();
11689        assert_eq!(
11690            src_delta.text.as_str(),
11691            // The lack indentation is a formatter bug.
11692            "\
11693sketch(on = XY) {
11694  point1 = point(at = [var 1, var 2])
11695  point2 = point(at = [var 3, var 4])
11696  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11697}
11698"
11699        );
11700        assert_eq!(
11701            scene_delta.new_graph.objects.len(),
11702            5,
11703            "{:#?}",
11704            scene_delta.new_graph.objects
11705        );
11706        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11707        let sketch = expect_sketch(sketch_object);
11708        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11709        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11710            panic!("Expected constraint object");
11711        };
11712        let Constraint::VerticalDistance(distance) = constraint else {
11713            panic!("Expected vertical distance constraint");
11714        };
11715        assert_eq!(distance.label_position, Some(label_position));
11716
11717        mock_ctx.close().await;
11718    }
11719
11720    #[tokio::test(flavor = "multi_thread")]
11721    async fn test_add_fixed_standalone_point() {
11722        let initial_source = "\
11723sketch(on = XY) {
11724  point(at = [var 1, var 2])
11725}
11726";
11727
11728        let program = Program::parse(initial_source).unwrap().0.unwrap();
11729
11730        let mut frontend = FrontendState::new();
11731
11732        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11733        let mock_ctx = ExecutorContext::new_mock(None).await;
11734        let version = Version(0);
11735
11736        frontend.hack_set_program(&ctx, program).await.unwrap();
11737        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11738        let sketch_id = sketch_object.id;
11739        let sketch = expect_sketch(sketch_object);
11740        let point_id = *sketch.segments.first().unwrap();
11741
11742        let (src_delta, scene_delta) = frontend
11743            .add_constraint(
11744                &mock_ctx,
11745                version,
11746                sketch_id,
11747                Constraint::Fixed(Fixed {
11748                    points: vec![FixedPoint {
11749                        point: point_id,
11750                        position: Point2d {
11751                            x: Number {
11752                                value: 2.0,
11753                                units: NumericSuffix::Mm,
11754                            },
11755                            y: Number {
11756                                value: 3.0,
11757                                units: NumericSuffix::Mm,
11758                            },
11759                        },
11760                    }],
11761                }),
11762            )
11763            .await
11764            .unwrap();
11765        assert_eq!(
11766            src_delta.text.as_str(),
11767            "\
11768sketch(on = XY) {
11769  point1 = point(at = [var 2, var 3])
11770  fixed([point1, [2mm, 3mm]])
11771}
11772"
11773        );
11774        assert_eq!(
11775            scene_delta.new_graph.objects.len(),
11776            4,
11777            "{:#?}",
11778            scene_delta.new_graph.objects
11779        );
11780
11781        ctx.close().await;
11782        mock_ctx.close().await;
11783    }
11784
11785    #[tokio::test(flavor = "multi_thread")]
11786    async fn test_add_fixed_multiple_points() {
11787        let initial_source = "\
11788sketch(on = XY) {
11789  point(at = [var 1, var 2])
11790  point(at = [var 3, var 4])
11791}
11792";
11793
11794        let program = Program::parse(initial_source).unwrap().0.unwrap();
11795
11796        let mut frontend = FrontendState::new();
11797
11798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11799        let mock_ctx = ExecutorContext::new_mock(None).await;
11800        let version = Version(0);
11801
11802        frontend.hack_set_program(&ctx, program).await.unwrap();
11803        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11804        let sketch_id = sketch_object.id;
11805        let sketch = expect_sketch(sketch_object);
11806        let point0_id = *sketch.segments.first().unwrap();
11807        let point1_id = *sketch.segments.get(1).unwrap();
11808
11809        let (src_delta, scene_delta) = frontend
11810            .add_constraint(
11811                &mock_ctx,
11812                version,
11813                sketch_id,
11814                Constraint::Fixed(Fixed {
11815                    points: vec![
11816                        FixedPoint {
11817                            point: point0_id,
11818                            position: Point2d {
11819                                x: Number {
11820                                    value: 2.0,
11821                                    units: NumericSuffix::Mm,
11822                                },
11823                                y: Number {
11824                                    value: 3.0,
11825                                    units: NumericSuffix::Mm,
11826                                },
11827                            },
11828                        },
11829                        FixedPoint {
11830                            point: point1_id,
11831                            position: Point2d {
11832                                x: Number {
11833                                    value: 4.0,
11834                                    units: NumericSuffix::Mm,
11835                                },
11836                                y: Number {
11837                                    value: 5.0,
11838                                    units: NumericSuffix::Mm,
11839                                },
11840                            },
11841                        },
11842                    ],
11843                }),
11844            )
11845            .await
11846            .unwrap();
11847        assert_eq!(
11848            src_delta.text.as_str(),
11849            "\
11850sketch(on = XY) {
11851  point1 = point(at = [var 2, var 3])
11852  point2 = point(at = [var 4, var 5])
11853  fixed([point1, [2mm, 3mm]])
11854  fixed([point2, [4mm, 5mm]])
11855}
11856"
11857        );
11858        assert_eq!(
11859            scene_delta.new_graph.objects.len(),
11860            6,
11861            "{:#?}",
11862            scene_delta.new_graph.objects
11863        );
11864
11865        ctx.close().await;
11866        mock_ctx.close().await;
11867    }
11868
11869    #[tokio::test(flavor = "multi_thread")]
11870    async fn test_add_fixed_owned_point() {
11871        let initial_source = "\
11872sketch(on = XY) {
11873  line(start = [var 1, var 2], end = [var 3, var 4])
11874}
11875";
11876
11877        let program = Program::parse(initial_source).unwrap().0.unwrap();
11878
11879        let mut frontend = FrontendState::new();
11880
11881        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11882        let mock_ctx = ExecutorContext::new_mock(None).await;
11883        let version = Version(0);
11884
11885        frontend.hack_set_program(&ctx, program).await.unwrap();
11886        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11887        let sketch_id = sketch_object.id;
11888        let sketch = expect_sketch(sketch_object);
11889        let line_start_id = *sketch.segments.first().unwrap();
11890
11891        let (src_delta, scene_delta) = frontend
11892            .add_constraint(
11893                &mock_ctx,
11894                version,
11895                sketch_id,
11896                Constraint::Fixed(Fixed {
11897                    points: vec![FixedPoint {
11898                        point: line_start_id,
11899                        position: Point2d {
11900                            x: Number {
11901                                value: 2.0,
11902                                units: NumericSuffix::Mm,
11903                            },
11904                            y: Number {
11905                                value: 3.0,
11906                                units: NumericSuffix::Mm,
11907                            },
11908                        },
11909                    }],
11910                }),
11911            )
11912            .await
11913            .unwrap();
11914        assert_eq!(
11915            src_delta.text.as_str(),
11916            "\
11917sketch(on = XY) {
11918  line1 = line(start = [var 2, var 3], end = [var 3, var 4])
11919  fixed([line1.start, [2mm, 3mm]])
11920}
11921"
11922        );
11923        assert_eq!(
11924            scene_delta.new_graph.objects.len(),
11925            6,
11926            "{:#?}",
11927            scene_delta.new_graph.objects
11928        );
11929
11930        ctx.close().await;
11931        mock_ctx.close().await;
11932    }
11933
11934    #[tokio::test(flavor = "multi_thread")]
11935    async fn test_radius_error_cases() {
11936        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11937        let mock_ctx = ExecutorContext::new_mock(None).await;
11938        let version = Version(0);
11939
11940        // Test: Single point should error
11941        let initial_source_point = "\
11942sketch(on = XY) {
11943  point(at = [var 1, var 2])
11944}
11945";
11946        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11947        let mut frontend_point = FrontendState::new();
11948        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11949        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11950        let sketch_id_point = sketch_object_point.id;
11951        let sketch_point = expect_sketch(sketch_object_point);
11952        let point_id = *sketch_point.segments.first().unwrap();
11953
11954        let constraint_point = Constraint::Radius(Radius {
11955            arc: point_id,
11956            radius: Number {
11957                value: 5.0,
11958                units: NumericSuffix::Mm,
11959            },
11960            label_position: None,
11961            source: Default::default(),
11962        });
11963        let result_point = frontend_point
11964            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11965            .await;
11966        assert!(result_point.is_err(), "Single point should error for radius");
11967
11968        // Test: Single line segment should error (only arc segments supported)
11969        let initial_source_line = "\
11970sketch(on = XY) {
11971  line(start = [var 1, var 2], end = [var 3, var 4])
11972}
11973";
11974        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11975        let mut frontend_line = FrontendState::new();
11976        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11977        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11978        let sketch_id_line = sketch_object_line.id;
11979        let sketch_line = expect_sketch(sketch_object_line);
11980        let line_id = *sketch_line.segments.first().unwrap();
11981
11982        let constraint_line = Constraint::Radius(Radius {
11983            arc: line_id,
11984            radius: Number {
11985                value: 5.0,
11986                units: NumericSuffix::Mm,
11987            },
11988            label_position: None,
11989            source: Default::default(),
11990        });
11991        let result_line = frontend_line
11992            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11993            .await;
11994        assert!(result_line.is_err(), "Single line segment should error for radius");
11995
11996        ctx.close().await;
11997        mock_ctx.close().await;
11998    }
11999
12000    #[tokio::test(flavor = "multi_thread")]
12001    async fn test_diameter_single_arc_segment() {
12002        let initial_source = "\
12003sketch(on = XY) {
12004  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12005}
12006";
12007
12008        let program = Program::parse(initial_source).unwrap().0.unwrap();
12009
12010        let mut frontend = FrontendState::new();
12011
12012        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12013        let mock_ctx = ExecutorContext::new_mock(None).await;
12014        let version = Version(0);
12015
12016        frontend.hack_set_program(&ctx, program).await.unwrap();
12017        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12018        let sketch_id = sketch_object.id;
12019        let sketch = expect_sketch(sketch_object);
12020        // Find the arc segment (not the points)
12021        let arc_id = sketch
12022            .segments
12023            .iter()
12024            .find(|&seg_id| {
12025                let obj = frontend.scene_graph.objects.get(seg_id.0);
12026                matches!(
12027                    obj.map(|o| &o.kind),
12028                    Some(ObjectKind::Segment {
12029                        segment: Segment::Arc(_)
12030                    })
12031                )
12032            })
12033            .unwrap();
12034
12035        let constraint = Constraint::Diameter(Diameter {
12036            arc: *arc_id,
12037            diameter: Number {
12038                value: 10.0,
12039                units: NumericSuffix::Mm,
12040            },
12041            label_position: None,
12042            source: Default::default(),
12043        });
12044        let (src_delta, scene_delta) = frontend
12045            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12046            .await
12047            .unwrap();
12048        assert_eq!(
12049            src_delta.text.as_str(),
12050            // The lack indentation is a formatter bug.
12051            "\
12052sketch(on = XY) {
12053  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12054  diameter(arc1) == 10mm
12055}
12056"
12057        );
12058        assert_eq!(
12059            scene_delta.new_graph.objects.len(),
12060            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12061            "{:#?}",
12062            scene_delta.new_graph.objects
12063        );
12064
12065        ctx.close().await;
12066        mock_ctx.close().await;
12067    }
12068
12069    #[tokio::test(flavor = "multi_thread")]
12070    async fn test_diameter_single_arc_segment_with_label_position() {
12071        let initial_source = "\
12072sketch(on = XY) {
12073  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12074}
12075";
12076
12077        let program = Program::parse(initial_source).unwrap().0.unwrap();
12078        let mut frontend = FrontendState::new();
12079        let mock_ctx = ExecutorContext::new_mock(None).await;
12080        let version = Version(0);
12081
12082        frontend.program = program.clone();
12083        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12084        frontend.update_state_after_exec(outcome, true);
12085        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12086        let sketch_id = sketch_object.id;
12087        let sketch = expect_sketch(sketch_object);
12088        let arc_id = sketch
12089            .segments
12090            .iter()
12091            .find(|&seg_id| {
12092                let obj = frontend.scene_graph.objects.get(seg_id.0);
12093                matches!(
12094                    obj.map(|o| &o.kind),
12095                    Some(ObjectKind::Segment {
12096                        segment: Segment::Arc(_)
12097                    })
12098                )
12099            })
12100            .unwrap();
12101
12102        let label_position = Point2d {
12103            x: Number {
12104                value: 10.0,
12105                units: NumericSuffix::Mm,
12106            },
12107            y: Number {
12108                value: 11.0,
12109                units: NumericSuffix::Mm,
12110            },
12111        };
12112        let constraint = Constraint::Diameter(Diameter {
12113            arc: *arc_id,
12114            diameter: Number {
12115                value: 10.0,
12116                units: NumericSuffix::Mm,
12117            },
12118            label_position: Some(label_position.clone()),
12119            source: Default::default(),
12120        });
12121        let (src_delta, scene_delta) = frontend
12122            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12123            .await
12124            .unwrap();
12125        assert_eq!(
12126            src_delta.text.as_str(),
12127            "\
12128sketch(on = XY) {
12129  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12130  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12131}
12132"
12133        );
12134
12135        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12136        let sketch = expect_sketch(sketch_object);
12137        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12138        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12139            panic!("Expected constraint object");
12140        };
12141        let Constraint::Diameter(diameter) = constraint else {
12142            panic!("Expected diameter constraint");
12143        };
12144        assert_eq!(diameter.label_position, Some(label_position));
12145
12146        mock_ctx.close().await;
12147    }
12148
12149    #[tokio::test(flavor = "multi_thread")]
12150    async fn test_edit_diameter_constraint_label_position() {
12151        let initial_source = "\
12152sketch(on = XY) {
12153  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12154  diameter(arc1) == 10mm
12155}
12156";
12157
12158        let program = Program::parse(initial_source).unwrap().0.unwrap();
12159        let mut frontend = FrontendState::new();
12160        let mock_ctx = ExecutorContext::new_mock(None).await;
12161        let version = Version(0);
12162
12163        frontend.program = program.clone();
12164        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12165        frontend.update_state_after_exec(outcome, true);
12166        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12167        let sketch_id = sketch_object.id;
12168        let sketch = expect_sketch(sketch_object);
12169        let constraint_id = sketch.constraints[0];
12170        let label_position = Point2d {
12171            x: Number {
12172                value: 10.0,
12173                units: NumericSuffix::Mm,
12174            },
12175            y: Number {
12176                value: 11.0,
12177                units: NumericSuffix::Mm,
12178            },
12179        };
12180
12181        let (src_delta, scene_delta) = frontend
12182            .edit_distance_constraint_label_position(
12183                &mock_ctx,
12184                version,
12185                sketch_id,
12186                constraint_id,
12187                label_position.clone(),
12188                vec![],
12189            )
12190            .await
12191            .unwrap();
12192        assert_eq!(
12193            src_delta.text.as_str(),
12194            "\
12195sketch(on = XY) {
12196  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12197  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12198}
12199"
12200        );
12201
12202        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12203        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12204            panic!("Expected constraint object");
12205        };
12206        let Constraint::Diameter(diameter) = constraint else {
12207            panic!("Expected diameter constraint");
12208        };
12209        assert_eq!(diameter.label_position, Some(label_position));
12210
12211        mock_ctx.close().await;
12212    }
12213
12214    #[tokio::test(flavor = "multi_thread")]
12215    async fn test_diameter_error_cases() {
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        // Test: Single point should error
12221        let initial_source_point = "\
12222sketch(on = XY) {
12223  point(at = [var 1, var 2])
12224}
12225";
12226        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12227        let mut frontend_point = FrontendState::new();
12228        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12229        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12230        let sketch_id_point = sketch_object_point.id;
12231        let sketch_point = expect_sketch(sketch_object_point);
12232        let point_id = *sketch_point.segments.first().unwrap();
12233
12234        let constraint_point = Constraint::Diameter(Diameter {
12235            arc: point_id,
12236            diameter: Number {
12237                value: 10.0,
12238                units: NumericSuffix::Mm,
12239            },
12240            label_position: None,
12241            source: Default::default(),
12242        });
12243        let result_point = frontend_point
12244            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12245            .await;
12246        assert!(result_point.is_err(), "Single point should error for diameter");
12247
12248        // Test: Single line segment should error (only arc segments supported)
12249        let initial_source_line = "\
12250sketch(on = XY) {
12251  line(start = [var 1, var 2], end = [var 3, var 4])
12252}
12253";
12254        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12255        let mut frontend_line = FrontendState::new();
12256        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12257        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12258        let sketch_id_line = sketch_object_line.id;
12259        let sketch_line = expect_sketch(sketch_object_line);
12260        let line_id = *sketch_line.segments.first().unwrap();
12261
12262        let constraint_line = Constraint::Diameter(Diameter {
12263            arc: line_id,
12264            diameter: Number {
12265                value: 10.0,
12266                units: NumericSuffix::Mm,
12267            },
12268            label_position: None,
12269            source: Default::default(),
12270        });
12271        let result_line = frontend_line
12272            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12273            .await;
12274        assert!(result_line.is_err(), "Single line segment should error for diameter");
12275
12276        ctx.close().await;
12277        mock_ctx.close().await;
12278    }
12279
12280    #[tokio::test(flavor = "multi_thread")]
12281    async fn test_line_horizontal() {
12282        let initial_source = "\
12283sketch(on = XY) {
12284  line(start = [var 1, var 2], end = [var 3, var 4])
12285}
12286";
12287
12288        let program = Program::parse(initial_source).unwrap().0.unwrap();
12289
12290        let mut frontend = FrontendState::new();
12291
12292        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12293        let mock_ctx = ExecutorContext::new_mock(None).await;
12294        let version = Version(0);
12295
12296        frontend.hack_set_program(&ctx, program).await.unwrap();
12297        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12298        let sketch_id = sketch_object.id;
12299        let sketch = expect_sketch(sketch_object);
12300        let line1_id = *sketch.segments.get(2).unwrap();
12301
12302        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12303        let (src_delta, scene_delta) = frontend
12304            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12305            .await
12306            .unwrap();
12307        assert_eq!(
12308            src_delta.text.as_str(),
12309            "\
12310sketch(on = XY) {
12311  line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12312  horizontal(line1)
12313}
12314"
12315        );
12316        assert_eq!(
12317            scene_delta.new_graph.objects.len(),
12318            6,
12319            "{:#?}",
12320            scene_delta.new_graph.objects
12321        );
12322
12323        ctx.close().await;
12324        mock_ctx.close().await;
12325    }
12326
12327    #[tokio::test(flavor = "multi_thread")]
12328    async fn test_control_point_spline_edge_horizontal() {
12329        let initial_source = "\
12330@settings(experimentalFeatures = allow)
12331splineSketch = sketch(on = XY) {
12332  controlPointSpline1 = controlPointSpline(points = [
12333    [var 0mm, var 0mm],
12334    [var 10mm, var 20mm],
12335    [var 20mm, var 0mm],
12336  ])
12337}
12338";
12339
12340        let program = Program::parse(initial_source).unwrap().0.unwrap();
12341
12342        let mut frontend = FrontendState::new();
12343
12344        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12345        let mock_ctx = ExecutorContext::new_mock(None).await;
12346        let version = Version(0);
12347
12348        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12349        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12350        let sketch_id = sketch_object.id;
12351        let sketch = expect_sketch(sketch_object);
12352        let spline_id = sketch
12353            .segments
12354            .iter()
12355            .copied()
12356            .find(|seg_id| {
12357                matches!(
12358                    &frontend.scene_graph.objects[seg_id.0].kind,
12359                    ObjectKind::Segment {
12360                        segment: Segment::ControlPointSpline(_)
12361                    }
12362                )
12363            })
12364            .expect("Expected a control point spline segment in sketch");
12365        let edge_id = frontend
12366            .scene_graph
12367            .objects
12368            .iter()
12369            .find_map(|obj| match &obj.kind {
12370                ObjectKind::Segment {
12371                    segment: Segment::Line(line),
12372                } if line.owner == Some(spline_id) => Some(obj.id),
12373                _ => None,
12374            })
12375            .expect("Expected an owned control-polygon edge");
12376
12377        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12378        let (src_delta, _) = frontend
12379            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12380            .await
12381            .unwrap();
12382        assert!(
12383            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12384            "Expected horizontal constraint on spline edge, got: {}",
12385            src_delta.text
12386        );
12387
12388        ctx.close().await;
12389        mock_ctx.close().await;
12390    }
12391
12392    #[tokio::test(flavor = "multi_thread")]
12393    async fn test_control_point_spline_edge_angle() {
12394        let initial_source = "\
12395@settings(experimentalFeatures = allow)
12396splineSketch = sketch(on = XY) {
12397  controlPointSpline1 = controlPointSpline(points = [
12398    [var 0mm, var 0mm],
12399    [var 10mm, var 20mm],
12400    [var 20mm, var 0mm],
12401  ])
12402
12403  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12404}
12405";
12406
12407        let program = Program::parse(initial_source).unwrap().0.unwrap();
12408
12409        let mut frontend = FrontendState::new();
12410
12411        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12412        let mock_ctx = ExecutorContext::new_mock(None).await;
12413        let version = Version(0);
12414
12415        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12416        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12417        let sketch_id = sketch_object.id;
12418        let sketch = expect_sketch(sketch_object);
12419        let spline_id = sketch
12420            .segments
12421            .iter()
12422            .copied()
12423            .find(|seg_id| {
12424                matches!(
12425                    &frontend.scene_graph.objects[seg_id.0].kind,
12426                    ObjectKind::Segment {
12427                        segment: Segment::ControlPointSpline(_)
12428                    }
12429                )
12430            })
12431            .expect("Expected a control point spline segment in sketch");
12432        let edge_id = frontend
12433            .scene_graph
12434            .objects
12435            .iter()
12436            .find_map(|obj| match &obj.kind {
12437                ObjectKind::Segment {
12438                    segment: Segment::Line(line),
12439                } if line.owner == Some(spline_id) => Some(obj.id),
12440                _ => None,
12441            })
12442            .expect("Expected an owned control-polygon edge");
12443        let line1_id = frontend
12444            .scene_graph
12445            .objects
12446            .iter()
12447            .find_map(|obj| match &obj.kind {
12448                ObjectKind::Segment {
12449                    segment: Segment::Line(line),
12450                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12451                _ => None,
12452            })
12453            .or_else(|| {
12454                sketch.segments.iter().copied().find(|seg_id| {
12455                    matches!(
12456                        &frontend.scene_graph.objects[seg_id.0].kind,
12457                        ObjectKind::Segment {
12458                            segment: Segment::Line(line),
12459                        } if line.owner.is_none()
12460                    )
12461                })
12462            })
12463            .expect("Expected a standalone line segment in sketch");
12464
12465        let constraint = Constraint::Angle(Angle {
12466            lines: vec![line1_id, edge_id],
12467            angle: Number {
12468                value: 30.0,
12469                units: NumericSuffix::Deg,
12470            },
12471            source: Default::default(),
12472        });
12473        let (src_delta, _) = frontend
12474            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12475            .await
12476            .unwrap();
12477        assert!(
12478            src_delta
12479                .text
12480                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12481            "Expected angle constraint on spline edge, got: {}",
12482            src_delta.text
12483        );
12484
12485        ctx.close().await;
12486        mock_ctx.close().await;
12487    }
12488
12489    #[tokio::test(flavor = "multi_thread")]
12490    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12491        let initial_source = "\
12492@settings(experimentalFeatures = allow)
12493splineSketch = sketch(on = XY) {
12494  spline1 = controlPointSpline(points = [
12495    [var 0mm, var 0mm],
12496    [var 10mm, var 20mm],
12497    [var 20mm, var 0mm],
12498  ])
12499  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12500  coincident([spline1.controls[1], spline1.edges[0]])
12501  coincident([spline1.controls[0], line1])
12502}
12503";
12504
12505        let program = Program::parse(initial_source).unwrap().0.unwrap();
12506
12507        let mut frontend = FrontendState::new();
12508
12509        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12510        let mock_ctx = ExecutorContext::new_mock(None).await;
12511
12512        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12513
12514        let ui_scene_graph = frontend.scene_graph_for_ui();
12515        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12516        let sketch = expect_sketch(sketch_object);
12517
12518        assert_eq!(
12519            sketch.constraints.len(),
12520            1,
12521            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12522        );
12523
12524        let visible_constraints = ui_scene_graph
12525            .objects
12526            .iter()
12527            .filter_map(|object| match &object.kind {
12528                ObjectKind::Constraint {
12529                    constraint: Constraint::Coincident(coincident),
12530                } => Some(coincident.clone()),
12531                _ => None,
12532            })
12533            .collect::<Vec<_>>();
12534
12535        assert_eq!(
12536            visible_constraints.len(),
12537            1,
12538            "Expected only one coincident constraint object in the UI scene graph"
12539        );
12540        assert_eq!(
12541            visible_constraints[0].get_segments().len(),
12542            2,
12543            "Expected the remaining visible coincident constraint to reference two segments"
12544        );
12545
12546        ctx.close().await;
12547        mock_ctx.close().await;
12548    }
12549
12550    #[tokio::test(flavor = "multi_thread")]
12551    async fn test_edit_control_point_spline_can_append_control_point() {
12552        let initial_source = "\
12553@settings(experimentalFeatures = allow)
12554splineSketch = sketch(on = XY) {
12555  controlPointSpline(points = [
12556    [var 0mm, var 0mm],
12557    [var 10mm, var 20mm],
12558    [var 20mm, var 0mm],
12559  ])
12560}
12561";
12562
12563        let program = Program::parse(initial_source).unwrap().0.unwrap();
12564
12565        let mut frontend = FrontendState::new();
12566
12567        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12568        let mock_ctx = ExecutorContext::new_mock(None).await;
12569        let version = Version(0);
12570
12571        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12573        let sketch_id = sketch_object.id;
12574        let sketch = expect_sketch(sketch_object);
12575        let spline_id = sketch
12576            .segments
12577            .iter()
12578            .copied()
12579            .find(|seg_id| {
12580                matches!(
12581                    &frontend.scene_graph.objects[seg_id.0].kind,
12582                    ObjectKind::Segment {
12583                        segment: Segment::ControlPointSpline(_)
12584                    }
12585                )
12586            })
12587            .expect("Expected a control point spline segment in sketch");
12588
12589        let ctor = ControlPointSplineCtor {
12590            points: vec![
12591                Point2d {
12592                    x: Expr::Var(Number {
12593                        value: 0.0,
12594                        units: NumericSuffix::Mm,
12595                    }),
12596                    y: Expr::Var(Number {
12597                        value: 0.0,
12598                        units: NumericSuffix::Mm,
12599                    }),
12600                },
12601                Point2d {
12602                    x: Expr::Var(Number {
12603                        value: 10.0,
12604                        units: NumericSuffix::Mm,
12605                    }),
12606                    y: Expr::Var(Number {
12607                        value: 20.0,
12608                        units: NumericSuffix::Mm,
12609                    }),
12610                },
12611                Point2d {
12612                    x: Expr::Var(Number {
12613                        value: 20.0,
12614                        units: NumericSuffix::Mm,
12615                    }),
12616                    y: Expr::Var(Number {
12617                        value: 0.0,
12618                        units: NumericSuffix::Mm,
12619                    }),
12620                },
12621                Point2d {
12622                    x: Expr::Var(Number {
12623                        value: 30.0,
12624                        units: NumericSuffix::Mm,
12625                    }),
12626                    y: Expr::Var(Number {
12627                        value: 10.0,
12628                        units: NumericSuffix::Mm,
12629                    }),
12630                },
12631            ],
12632            construction: None,
12633        };
12634
12635        let segments = vec![ExistingSegmentCtor {
12636            id: spline_id,
12637            ctor: SegmentCtor::ControlPointSpline(ctor),
12638        }];
12639        let (src_delta, scene_delta) = frontend
12640            .edit_segments(&mock_ctx, version, sketch_id, segments)
12641            .await
12642            .unwrap();
12643
12644        assert!(
12645            src_delta.text.contains("[var 30mm, var 10mm]"),
12646            "Expected appended spline control point in source, got: {}",
12647            src_delta.text
12648        );
12649
12650        assert!(
12651            scene_delta.invalidates_ids,
12652            "Expected appending a spline control point to invalidate ids"
12653        );
12654        let updated_spline = scene_delta
12655            .new_graph
12656            .objects
12657            .iter()
12658            .find_map(|obj| match &obj.kind {
12659                ObjectKind::Segment {
12660                    segment: Segment::ControlPointSpline(updated_spline),
12661                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12662                _ => None,
12663            })
12664            .expect("Expected edited scene graph to contain a four-point control point spline");
12665        assert_eq!(
12666            updated_spline.controls.len(),
12667            4,
12668            "Expected edited spline to expose four control points"
12669        );
12670
12671        ctx.close().await;
12672        mock_ctx.close().await;
12673    }
12674
12675    #[tokio::test(flavor = "multi_thread")]
12676    async fn test_line_vertical() {
12677        let initial_source = "\
12678sketch(on = XY) {
12679  line(start = [var 1, var 2], end = [var 3, var 4])
12680}
12681";
12682
12683        let program = Program::parse(initial_source).unwrap().0.unwrap();
12684
12685        let mut frontend = FrontendState::new();
12686
12687        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12688        let mock_ctx = ExecutorContext::new_mock(None).await;
12689        let version = Version(0);
12690
12691        frontend.hack_set_program(&ctx, program).await.unwrap();
12692        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12693        let sketch_id = sketch_object.id;
12694        let sketch = expect_sketch(sketch_object);
12695        let line1_id = *sketch.segments.get(2).unwrap();
12696
12697        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12698        let (src_delta, scene_delta) = frontend
12699            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12700            .await
12701            .unwrap();
12702        assert_eq!(
12703            src_delta.text.as_str(),
12704            "\
12705sketch(on = XY) {
12706  line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12707  vertical(line1)
12708}
12709"
12710        );
12711        assert_eq!(
12712            scene_delta.new_graph.objects.len(),
12713            6,
12714            "{:#?}",
12715            scene_delta.new_graph.objects
12716        );
12717
12718        ctx.close().await;
12719        mock_ctx.close().await;
12720    }
12721
12722    #[tokio::test(flavor = "multi_thread")]
12723    async fn test_points_vertical() {
12724        let initial_source = "\
12725sketch001 = sketch(on = XY) {
12726  p0 = point(at = [var -2.23mm, var 3.1mm])
12727  pf = point(at = [4, 4])
12728}
12729";
12730
12731        let program = Program::parse(initial_source).unwrap().0.unwrap();
12732
12733        let mut frontend = FrontendState::new();
12734
12735        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12736        let mock_ctx = ExecutorContext::new_mock(None).await;
12737        let version = Version(0);
12738
12739        frontend.hack_set_program(&ctx, program).await.unwrap();
12740        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12741        let sketch_id = sketch_object.id;
12742        let sketch = expect_sketch(sketch_object);
12743        let point_ids = vec![
12744            sketch.segments.first().unwrap().to_owned(),
12745            sketch.segments.get(1).unwrap().to_owned(),
12746        ];
12747
12748        let constraint = Constraint::Vertical(Vertical::Points {
12749            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12750        });
12751        let (src_delta, scene_delta) = frontend
12752            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12753            .await
12754            .unwrap();
12755        assert_eq!(
12756            src_delta.text.as_str(),
12757            "\
12758sketch001 = sketch(on = XY) {
12759  p0 = point(at = [var 4mm, var 3.1mm])
12760  pf = point(at = [4, 4])
12761  vertical([p0, pf])
12762}
12763"
12764        );
12765        assert_eq!(
12766            scene_delta.new_graph.objects.len(),
12767            5,
12768            "{:#?}",
12769            scene_delta.new_graph.objects
12770        );
12771
12772        ctx.close().await;
12773        mock_ctx.close().await;
12774    }
12775
12776    #[tokio::test(flavor = "multi_thread")]
12777    async fn test_points_horizontal() {
12778        let initial_source = "\
12779sketch001 = sketch(on = XY) {
12780  p0 = point(at = [var -2.23mm, var 3.1mm])
12781  pf = point(at = [4, 4])
12782}
12783";
12784
12785        let program = Program::parse(initial_source).unwrap().0.unwrap();
12786
12787        let mut frontend = FrontendState::new();
12788
12789        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12790        let mock_ctx = ExecutorContext::new_mock(None).await;
12791        let version = Version(0);
12792
12793        frontend.hack_set_program(&ctx, program).await.unwrap();
12794        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12795        let sketch_id = sketch_object.id;
12796        let sketch = expect_sketch(sketch_object);
12797        let point_ids = vec![
12798            sketch.segments.first().unwrap().to_owned(),
12799            sketch.segments.get(1).unwrap().to_owned(),
12800        ];
12801
12802        let constraint = Constraint::Horizontal(Horizontal::Points {
12803            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12804        });
12805        let (src_delta, scene_delta) = frontend
12806            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12807            .await
12808            .unwrap();
12809        assert_eq!(
12810            src_delta.text.as_str(),
12811            "\
12812sketch001 = sketch(on = XY) {
12813  p0 = point(at = [var -2.23mm, var 4mm])
12814  pf = point(at = [4, 4])
12815  horizontal([p0, pf])
12816}
12817"
12818        );
12819        assert_eq!(
12820            scene_delta.new_graph.objects.len(),
12821            5,
12822            "{:#?}",
12823            scene_delta.new_graph.objects
12824        );
12825
12826        ctx.close().await;
12827        mock_ctx.close().await;
12828    }
12829
12830    #[tokio::test(flavor = "multi_thread")]
12831    async fn test_point_horizontal_with_origin() {
12832        let initial_source = "\
12833sketch001 = sketch(on = XY) {
12834  p0 = point(at = [var -2.23mm, var 3.1mm])
12835}
12836";
12837
12838        let program = Program::parse(initial_source).unwrap().0.unwrap();
12839
12840        let mut frontend = FrontendState::new();
12841
12842        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12843        let mock_ctx = ExecutorContext::new_mock(None).await;
12844        let version = Version(0);
12845
12846        frontend.hack_set_program(&ctx, program).await.unwrap();
12847        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12848        let sketch_id = sketch_object.id;
12849        let sketch = expect_sketch(sketch_object);
12850        let point_id = *sketch.segments.first().unwrap();
12851
12852        let constraint = Constraint::Horizontal(Horizontal::Points {
12853            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12854        });
12855        let (src_delta, scene_delta) = frontend
12856            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12857            .await
12858            .unwrap();
12859        assert_eq!(
12860            src_delta.text.as_str(),
12861            "\
12862sketch001 = sketch(on = XY) {
12863  p0 = point(at = [var -2.23mm, var 0mm])
12864  horizontal([p0, ORIGIN])
12865}
12866"
12867        );
12868        assert_eq!(
12869            scene_delta.new_graph.objects.len(),
12870            4,
12871            "{:#?}",
12872            scene_delta.new_graph.objects
12873        );
12874
12875        ctx.close().await;
12876        mock_ctx.close().await;
12877    }
12878
12879    #[tokio::test(flavor = "multi_thread")]
12880    async fn test_lines_equal_length() {
12881        let initial_source = "\
12882sketch(on = XY) {
12883  line(start = [var 1, var 2], end = [var 3, var 4])
12884  line(start = [var 5, var 6], end = [var 7, var 8])
12885}
12886";
12887
12888        let program = Program::parse(initial_source).unwrap().0.unwrap();
12889
12890        let mut frontend = FrontendState::new();
12891
12892        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12893        let mock_ctx = ExecutorContext::new_mock(None).await;
12894        let version = Version(0);
12895
12896        frontend.hack_set_program(&ctx, program).await.unwrap();
12897        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12898        let sketch_id = sketch_object.id;
12899        let sketch = expect_sketch(sketch_object);
12900        let line1_id = *sketch.segments.get(2).unwrap();
12901        let line2_id = *sketch.segments.get(5).unwrap();
12902
12903        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12904            lines: vec![line1_id, line2_id],
12905        });
12906        let (src_delta, scene_delta) = frontend
12907            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12908            .await
12909            .unwrap();
12910        assert_eq!(
12911            src_delta.text.as_str(),
12912            "\
12913sketch(on = XY) {
12914  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12915  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12916  equalLength([line1, line2])
12917}
12918"
12919        );
12920        assert_eq!(
12921            scene_delta.new_graph.objects.len(),
12922            9,
12923            "{:#?}",
12924            scene_delta.new_graph.objects
12925        );
12926
12927        ctx.close().await;
12928        mock_ctx.close().await;
12929    }
12930
12931    #[tokio::test(flavor = "multi_thread")]
12932    async fn test_add_constraint_multi_line_equal_length() {
12933        let initial_source = "\
12934sketch(on = XY) {
12935  line(start = [var 1, var 2], end = [var 3, var 4])
12936  line(start = [var 5, var 6], end = [var 7, var 8])
12937  line(start = [var 9, var 10], end = [var 11, var 12])
12938}
12939";
12940
12941        let program = Program::parse(initial_source).unwrap().0.unwrap();
12942
12943        let mut frontend = FrontendState::new();
12944        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12945        let mock_ctx = ExecutorContext::new_mock(None).await;
12946        let version = Version(0);
12947
12948        frontend.hack_set_program(&ctx, program).await.unwrap();
12949        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12950        let sketch_id = sketch_object.id;
12951        let sketch = expect_sketch(sketch_object);
12952        let line1_id = *sketch.segments.get(2).unwrap();
12953        let line2_id = *sketch.segments.get(5).unwrap();
12954        let line3_id = *sketch.segments.get(8).unwrap();
12955
12956        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12957            lines: vec![line1_id, line2_id, line3_id],
12958        });
12959        let (src_delta, scene_delta) = frontend
12960            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12961            .await
12962            .unwrap();
12963        assert_eq!(
12964            src_delta.text.as_str(),
12965            "\
12966sketch(on = XY) {
12967  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12968  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12969  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12970  equalLength([line1, line2, line3])
12971}
12972"
12973        );
12974        let constraints = scene_delta
12975            .new_graph
12976            .objects
12977            .iter()
12978            .filter_map(|obj| {
12979                let ObjectKind::Constraint { constraint } = &obj.kind else {
12980                    return None;
12981                };
12982                Some(constraint)
12983            })
12984            .collect::<Vec<_>>();
12985
12986        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12987        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12988            panic!("expected equal length constraint, got {:?}", constraints[0]);
12989        };
12990        assert_eq!(lines_equal_length.lines.len(), 3);
12991
12992        ctx.close().await;
12993        mock_ctx.close().await;
12994    }
12995
12996    #[tokio::test(flavor = "multi_thread")]
12997    async fn test_lines_parallel() {
12998        let initial_source = "\
12999sketch(on = XY) {
13000  line(start = [var 1, var 2], end = [var 3, var 4])
13001  line(start = [var 5, var 6], end = [var 7, var 8])
13002}
13003";
13004
13005        let program = Program::parse(initial_source).unwrap().0.unwrap();
13006
13007        let mut frontend = FrontendState::new();
13008
13009        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13010        let mock_ctx = ExecutorContext::new_mock(None).await;
13011        let version = Version(0);
13012
13013        frontend.hack_set_program(&ctx, program).await.unwrap();
13014        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13015        let sketch_id = sketch_object.id;
13016        let sketch = expect_sketch(sketch_object);
13017        let line1_id = *sketch.segments.get(2).unwrap();
13018        let line2_id = *sketch.segments.get(5).unwrap();
13019
13020        let constraint = Constraint::Parallel(Parallel {
13021            lines: vec![line1_id, line2_id],
13022        });
13023        let (src_delta, scene_delta) = frontend
13024            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13025            .await
13026            .unwrap();
13027        assert_eq!(
13028            src_delta.text.as_str(),
13029            "\
13030sketch(on = XY) {
13031  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13032  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13033  parallel([line1, line2])
13034}
13035"
13036        );
13037        assert_eq!(
13038            scene_delta.new_graph.objects.len(),
13039            9,
13040            "{:#?}",
13041            scene_delta.new_graph.objects
13042        );
13043
13044        ctx.close().await;
13045        mock_ctx.close().await;
13046    }
13047
13048    #[tokio::test(flavor = "multi_thread")]
13049    async fn test_lines_parallel_multiline() {
13050        let initial_source = "\
13051sketch(on = XY) {
13052  line(start = [var 1, var 2], end = [var 3, var 4])
13053  line(start = [var 5, var 6], end = [var 7, var 8])
13054  line(start = [var 9, var 10], end = [var 11, var 12])
13055}
13056";
13057
13058        let program = Program::parse(initial_source).unwrap().0.unwrap();
13059
13060        let mut frontend = FrontendState::new();
13061
13062        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13063        let mock_ctx = ExecutorContext::new_mock(None).await;
13064        let version = Version(0);
13065
13066        frontend.hack_set_program(&ctx, program).await.unwrap();
13067        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13068        let sketch_id = sketch_object.id;
13069        let sketch = expect_sketch(sketch_object);
13070        let line1_id = *sketch.segments.get(2).unwrap();
13071        let line2_id = *sketch.segments.get(5).unwrap();
13072        let line3_id = *sketch.segments.get(8).unwrap();
13073
13074        let constraint = Constraint::Parallel(Parallel {
13075            lines: vec![line1_id, line2_id, line3_id],
13076        });
13077        let (src_delta, scene_delta) = frontend
13078            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13079            .await
13080            .unwrap();
13081        assert_eq!(
13082            src_delta.text.as_str(),
13083            "\
13084sketch(on = XY) {
13085  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13086  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13087  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13088  parallel([line1, line2, line3])
13089}
13090"
13091        );
13092
13093        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13094        let sketch = expect_sketch(sketch_object);
13095        assert_eq!(sketch.constraints.len(), 1);
13096
13097        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13098        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13099            panic!("Expected constraint object");
13100        };
13101        let Constraint::Parallel(parallel) = constraint else {
13102            panic!("Expected parallel constraint");
13103        };
13104        assert_eq!(parallel.lines.len(), 3);
13105
13106        ctx.close().await;
13107        mock_ctx.close().await;
13108    }
13109
13110    #[tokio::test(flavor = "multi_thread")]
13111    async fn test_lines_perpendicular() {
13112        let initial_source = "\
13113sketch(on = XY) {
13114  line(start = [var 1, var 2], end = [var 3, var 4])
13115  line(start = [var 5, var 6], end = [var 7, var 8])
13116}
13117";
13118
13119        let program = Program::parse(initial_source).unwrap().0.unwrap();
13120
13121        let mut frontend = FrontendState::new();
13122
13123        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13124        let mock_ctx = ExecutorContext::new_mock(None).await;
13125        let version = Version(0);
13126
13127        frontend.hack_set_program(&ctx, program).await.unwrap();
13128        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13129        let sketch_id = sketch_object.id;
13130        let sketch = expect_sketch(sketch_object);
13131        let line1_id = *sketch.segments.get(2).unwrap();
13132        let line2_id = *sketch.segments.get(5).unwrap();
13133
13134        let constraint = Constraint::Perpendicular(Perpendicular {
13135            lines: vec![line1_id, line2_id],
13136        });
13137        let (src_delta, scene_delta) = frontend
13138            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13139            .await
13140            .unwrap();
13141        assert_eq!(
13142            src_delta.text.as_str(),
13143            "\
13144sketch(on = XY) {
13145  line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13146  line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13147  perpendicular([line1, line2])
13148}
13149"
13150        );
13151        assert_eq!(
13152            scene_delta.new_graph.objects.len(),
13153            9,
13154            "{:#?}",
13155            scene_delta.new_graph.objects
13156        );
13157
13158        ctx.close().await;
13159        mock_ctx.close().await;
13160    }
13161
13162    #[tokio::test(flavor = "multi_thread")]
13163    async fn test_lines_angle() {
13164        let initial_source = "\
13165sketch(on = XY) {
13166  line(start = [var 1, var 2], end = [var 3, var 4])
13167  line(start = [var 5, var 6], end = [var 7, var 8])
13168}
13169";
13170
13171        let program = Program::parse(initial_source).unwrap().0.unwrap();
13172
13173        let mut frontend = FrontendState::new();
13174
13175        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13176        let mock_ctx = ExecutorContext::new_mock(None).await;
13177        let version = Version(0);
13178
13179        frontend.hack_set_program(&ctx, program).await.unwrap();
13180        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13181        let sketch_id = sketch_object.id;
13182        let sketch = expect_sketch(sketch_object);
13183        let line1_id = *sketch.segments.get(2).unwrap();
13184        let line2_id = *sketch.segments.get(5).unwrap();
13185
13186        let constraint = Constraint::Angle(Angle {
13187            lines: vec![line1_id, line2_id],
13188            angle: Number {
13189                value: 30.0,
13190                units: NumericSuffix::Deg,
13191            },
13192            source: Default::default(),
13193        });
13194        let (src_delta, scene_delta) = frontend
13195            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13196            .await
13197            .unwrap();
13198        assert_eq!(
13199            src_delta.text.as_str(),
13200            // The lack indentation is a formatter bug.
13201            "\
13202sketch(on = XY) {
13203  line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13204  line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13205  angle([line1, line2]) == 30deg
13206}
13207"
13208        );
13209        assert_eq!(
13210            scene_delta.new_graph.objects.len(),
13211            9,
13212            "{:#?}",
13213            scene_delta.new_graph.objects
13214        );
13215
13216        ctx.close().await;
13217        mock_ctx.close().await;
13218    }
13219
13220    #[tokio::test(flavor = "multi_thread")]
13221    async fn test_segments_tangent() {
13222        let initial_source = "\
13223sketch(on = XY) {
13224  line(start = [var 1, var 2], end = [var 3, var 4])
13225  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13226}
13227";
13228
13229        let program = Program::parse(initial_source).unwrap().0.unwrap();
13230
13231        let mut frontend = FrontendState::new();
13232
13233        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13234        let mock_ctx = ExecutorContext::new_mock(None).await;
13235        let version = Version(0);
13236
13237        frontend.hack_set_program(&ctx, program).await.unwrap();
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 line1_id = *sketch.segments.get(2).unwrap();
13242        let arc1_id = *sketch.segments.get(6).unwrap();
13243
13244        let constraint = Constraint::Tangent(Tangent {
13245            input: vec![line1_id, arc1_id],
13246        });
13247        let (src_delta, scene_delta) = frontend
13248            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13249            .await
13250            .unwrap();
13251        assert_eq!(
13252            src_delta.text.as_str(),
13253            "\
13254sketch(on = XY) {
13255  line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13256  arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13257  tangent([line1, arc1])
13258}
13259"
13260        );
13261        assert_eq!(
13262            scene_delta.new_graph.objects.len(),
13263            10,
13264            "{:#?}",
13265            scene_delta.new_graph.objects
13266        );
13267
13268        ctx.close().await;
13269        mock_ctx.close().await;
13270    }
13271
13272    #[tokio::test(flavor = "multi_thread")]
13273    async fn test_point_midpoint() {
13274        let initial_source = "\
13275sketch(on = XY) {
13276  point(at = [var 1, var 1])
13277  line(start = [var 0, var 0], end = [var 6, var 4])
13278}
13279";
13280
13281        let program = Program::parse(initial_source).unwrap().0.unwrap();
13282
13283        let mut frontend = FrontendState::new();
13284
13285        let ctx = ExecutorContext::new_mock(None).await;
13286        let version = Version(0);
13287
13288        frontend.program = program.clone();
13289        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13290        frontend.update_state_after_exec(outcome, true);
13291        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13292        let sketch_id = sketch_object.id;
13293        let sketch = expect_sketch(sketch_object);
13294        let point_id = *sketch.segments.first().unwrap();
13295        let line_id = *sketch.segments.get(3).unwrap();
13296
13297        let constraint = Constraint::Midpoint(Midpoint {
13298            point: ConstraintSegment::from(point_id),
13299            segment: line_id,
13300        });
13301        let (src_delta, scene_delta) = frontend
13302            .add_constraint(&ctx, version, sketch_id, constraint)
13303            .await
13304            .unwrap();
13305        assert_eq!(
13306            src_delta.text.as_str(),
13307            "\
13308sketch(on = XY) {
13309  point1 = point(at = [var 2.33, var 1.67])
13310  line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13311  midpoint(line1, point = point1)
13312}
13313"
13314        );
13315        assert_eq!(
13316            scene_delta.new_graph.objects.len(),
13317            7,
13318            "{:#?}",
13319            scene_delta.new_graph.objects
13320        );
13321
13322        ctx.close().await;
13323    }
13324
13325    #[tokio::test(flavor = "multi_thread")]
13326    async fn test_segments_symmetric() {
13327        let initial_source = "\
13328sketch(on = XY) {
13329  line(start = [var 0, var 0], end = [var 0, var 4])
13330  line(start = [var 4, var 0], end = [var 4, var 4])
13331  line(start = [var 2, var -1], end = [var 2, var 5])
13332}
13333";
13334
13335        let program = Program::parse(initial_source).unwrap().0.unwrap();
13336
13337        let mut frontend = FrontendState::new();
13338
13339        let ctx = ExecutorContext::new_mock(None).await;
13340        let version = Version(0);
13341
13342        frontend.program = program.clone();
13343        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13344        frontend.update_state_after_exec(outcome, true);
13345        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13346        let sketch_id = sketch_object.id;
13347        let sketch = expect_sketch(sketch_object);
13348        let line1_id = *sketch.segments.get(2).unwrap();
13349        let line2_id = *sketch.segments.get(5).unwrap();
13350        let axis_id = *sketch.segments.get(8).unwrap();
13351
13352        let constraint = Constraint::Symmetric(Symmetric {
13353            input: vec![line1_id, line2_id],
13354            axis: axis_id,
13355        });
13356        let (src_delta, scene_delta) = frontend
13357            .add_constraint(&ctx, version, sketch_id, constraint)
13358            .await
13359            .unwrap();
13360        assert_eq!(
13361            src_delta.text.as_str(),
13362            "\
13363sketch(on = XY) {
13364  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13365  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13366  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13367  symmetric([line1, line2], axis = line3)
13368}
13369"
13370        );
13371        assert_eq!(
13372            scene_delta.new_graph.objects.len(),
13373            12,
13374            "{:#?}",
13375            scene_delta.new_graph.objects
13376        );
13377
13378        ctx.close().await;
13379    }
13380
13381    #[tokio::test(flavor = "multi_thread")]
13382    async fn test_point_arc_midpoint() {
13383        let initial_source = "\
13384sketch(on = XY) {
13385  point(at = [var 6, var 3])
13386  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13387}
13388";
13389
13390        let program = Program::parse(initial_source).unwrap().0.unwrap();
13391
13392        let mut frontend = FrontendState::new();
13393
13394        let ctx = ExecutorContext::new_mock(None).await;
13395        let version = Version(0);
13396
13397        frontend.program = program.clone();
13398        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13399        frontend.update_state_after_exec(outcome, true);
13400        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13401        let sketch_id = sketch_object.id;
13402        let sketch = expect_sketch(sketch_object);
13403        let point_id = *sketch.segments.first().unwrap();
13404        let arc_id = *sketch.segments.get(4).unwrap();
13405
13406        let constraint = Constraint::Midpoint(Midpoint {
13407            point: ConstraintSegment::from(point_id),
13408            segment: arc_id,
13409        });
13410        let (src_delta, scene_delta) = frontend
13411            .add_constraint(&ctx, version, sketch_id, constraint)
13412            .await
13413            .unwrap();
13414        assert_eq!(
13415            src_delta.text.as_str(),
13416            "\
13417sketch(on = XY) {
13418  point1 = point(at = [var 6, var 2.35])
13419  arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13420  midpoint(arc1, point = point1)
13421}
13422"
13423        );
13424        assert_eq!(
13425            scene_delta.new_graph.objects.len(),
13426            8,
13427            "{:#?}",
13428            scene_delta.new_graph.objects
13429        );
13430
13431        ctx.close().await;
13432    }
13433
13434    #[tokio::test(flavor = "multi_thread")]
13435    async fn test_origin_line_midpoint() {
13436        let initial_source = "\
13437sketch(on = XY) {
13438  line(start = [var 0, var 0], end = [var 6, var 4])
13439}
13440";
13441
13442        let program = Program::parse(initial_source).unwrap().0.unwrap();
13443
13444        let mut frontend = FrontendState::new();
13445
13446        let ctx = ExecutorContext::new_mock(None).await;
13447        let version = Version(0);
13448
13449        frontend.program = program.clone();
13450        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13451        frontend.update_state_after_exec(outcome, true);
13452        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13453        let sketch_id = sketch_object.id;
13454        let sketch = expect_sketch(sketch_object);
13455        let line_id = *sketch.segments.get(2).unwrap();
13456
13457        let constraint = Constraint::Midpoint(Midpoint {
13458            point: ConstraintSegment::ORIGIN,
13459            segment: line_id,
13460        });
13461        let (src_delta, scene_delta) = frontend
13462            .add_constraint(&ctx, version, sketch_id, constraint)
13463            .await
13464            .unwrap();
13465        assert_eq!(
13466            src_delta.text.as_str(),
13467            "\
13468sketch(on = XY) {
13469  line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13470  midpoint(line1, point = ORIGIN)
13471}
13472"
13473        );
13474        assert_eq!(
13475            scene_delta.new_graph.objects.len(),
13476            6,
13477            "{:#?}",
13478            scene_delta.new_graph.objects
13479        );
13480
13481        ctx.close().await;
13482    }
13483
13484    #[tokio::test(flavor = "multi_thread")]
13485    async fn test_origin_arc_midpoint() {
13486        let initial_source = "\
13487sketch(on = XY) {
13488  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13489}
13490";
13491
13492        let program = Program::parse(initial_source).unwrap().0.unwrap();
13493
13494        let mut frontend = FrontendState::new();
13495
13496        let ctx = ExecutorContext::new_mock(None).await;
13497        let version = Version(0);
13498
13499        frontend.program = program.clone();
13500        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13501        frontend.update_state_after_exec(outcome, true);
13502        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13503        let sketch_id = sketch_object.id;
13504        let sketch = expect_sketch(sketch_object);
13505        let arc_id = *sketch.segments.get(3).unwrap();
13506
13507        let constraint = Constraint::Midpoint(Midpoint {
13508            point: ConstraintSegment::ORIGIN,
13509            segment: arc_id,
13510        });
13511        let (src_delta, scene_delta) = frontend
13512            .add_constraint(&ctx, version, sketch_id, constraint)
13513            .await
13514            .unwrap();
13515        assert_eq!(
13516            src_delta.text.as_str(),
13517            "\
13518sketch(on = XY) {
13519  arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13520  midpoint(arc1, point = ORIGIN)
13521}
13522"
13523        );
13524        assert_eq!(
13525            scene_delta.new_graph.objects.len(),
13526            7,
13527            "{:#?}",
13528            scene_delta.new_graph.objects
13529        );
13530
13531        ctx.close().await;
13532    }
13533
13534    #[tokio::test(flavor = "multi_thread")]
13535    async fn test_segments_symmetric_arcs() {
13536        let initial_source = "\
13537sketch(on = XY) {
13538  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13539  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13540  line(start = [var 0, var -10], end = [var 0, var 10])
13541}
13542";
13543
13544        let program = Program::parse(initial_source).unwrap().0.unwrap();
13545
13546        let mut frontend = FrontendState::new();
13547
13548        let ctx = ExecutorContext::new_mock(None).await;
13549        let version = Version(0);
13550
13551        frontend.program = program.clone();
13552        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13553        frontend.update_state_after_exec(outcome, true);
13554        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13555        let sketch_id = sketch_object.id;
13556        let sketch = expect_sketch(sketch_object);
13557        let arc1_id = *sketch.segments.get(3).unwrap();
13558        let arc2_id = *sketch.segments.get(7).unwrap();
13559        let axis_id = *sketch.segments.get(10).unwrap();
13560
13561        let constraint = Constraint::Symmetric(Symmetric {
13562            input: vec![arc1_id, arc2_id],
13563            axis: axis_id,
13564        });
13565        let (src_delta, scene_delta) = frontend
13566            .add_constraint(&ctx, version, sketch_id, constraint)
13567            .await
13568            .unwrap();
13569        assert_eq!(
13570            src_delta.text.as_str(),
13571            "\
13572sketch(on = XY) {
13573  arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13574  arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13575  line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13576  symmetric([arc1, arc2], axis = line1)
13577}
13578"
13579        );
13580        assert_eq!(
13581            scene_delta.new_graph.objects.len(),
13582            14,
13583            "{:#?}",
13584            scene_delta.new_graph.objects
13585        );
13586
13587        ctx.close().await;
13588    }
13589
13590    #[tokio::test(flavor = "multi_thread")]
13591    async fn test_sketch_on_face_simple() {
13592        let initial_source = "\
13593len = 2mm
13594cube = startSketchOn(XY)
13595  |> startProfile(at = [0, 0])
13596  |> line(end = [len, 0], tag = $side)
13597  |> line(end = [0, len])
13598  |> line(end = [-len, 0])
13599  |> line(end = [0, -len])
13600  |> close()
13601  |> extrude(length = len)
13602
13603face = faceOf(cube, face = side)
13604";
13605
13606        let program = Program::parse(initial_source).unwrap().0.unwrap();
13607
13608        let mut frontend = FrontendState::new();
13609
13610        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13611        let mock_ctx = ExecutorContext::new_mock(None).await;
13612        let version = Version(0);
13613
13614        frontend.hack_set_program(&ctx, program).await.unwrap();
13615        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13616        let face_id = face_object.id;
13617
13618        let sketch_args = SketchCtor {
13619            on: Plane::Object(face_id),
13620        };
13621        let (_src_delta, scene_delta, sketch_id) = frontend
13622            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13623            .await
13624            .unwrap();
13625        assert_eq!(sketch_id, ObjectId(2));
13626        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13627        let sketch_object = &scene_delta.new_graph.objects[2];
13628        assert_eq!(sketch_object.id, ObjectId(2));
13629        assert_eq!(
13630            sketch_object.kind,
13631            ObjectKind::Sketch(Sketch {
13632                args: SketchCtor {
13633                    on: Plane::Object(face_id),
13634                },
13635                plane: face_id,
13636                segments: vec![],
13637                constraints: vec![],
13638            })
13639        );
13640        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13641
13642        ctx.close().await;
13643        mock_ctx.close().await;
13644    }
13645
13646    #[tokio::test(flavor = "multi_thread")]
13647    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13648        let initial_source = "\
13649s = sketch(on = YZ) {
13650  line1 = line(start = [0, 0], end = [0, 1])
13651  line2 = line(start = [0, 1], end = [1, 1])
13652  line3 = line(start = [1, 1], end = [0, 0])
13653}
13654region001 = region(point = [0.1, 0.1], sketch = s)
13655extrude001 = extrude(region001, length = 5)
13656";
13657
13658        let program = Program::parse(initial_source).unwrap().0.unwrap();
13659
13660        let mut frontend = FrontendState::new();
13661        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13662        let version = Version(0);
13663
13664        frontend.hack_set_program(&ctx, program).await.unwrap();
13665        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13666
13667        let sketch_args = SketchCtor {
13668            on: Plane::Object(wall_object_id),
13669        };
13670        let (src_delta, _scene_delta, _sketch_id) = frontend
13671            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13672            .await
13673            .unwrap();
13674        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13675
13676        ctx.close().await;
13677    }
13678
13679    #[tokio::test(flavor = "multi_thread")]
13680    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13681        let initial_source = "\
13682sketch001 = sketch(on = YZ) {
13683  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13684  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13685  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13686  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13687  coincident([line1.end, line2.start])
13688  coincident([line2.end, line3.start])
13689  coincident([line3.end, line4.start])
13690  coincident([line4.end, line1.start])
13691  parallel([line2, line4])
13692  parallel([line3, line1])
13693  perpendicular([line1, line2])
13694  horizontal(line3)
13695  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13696}
13697region001 = region(point = [3.1, 3.74], sketch = sketch001)
13698extrude001 = extrude(region001, length = 5)
13699";
13700
13701        let program = Program::parse(initial_source).unwrap().0.unwrap();
13702
13703        let mut frontend = FrontendState::new();
13704        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13705        let version = Version(0);
13706
13707        frontend.hack_set_program(&ctx, program).await.unwrap();
13708        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13709
13710        let sketch_args = SketchCtor {
13711            on: Plane::Object(wall_object_id),
13712        };
13713        let (src_delta, _scene_delta, _sketch_id) = frontend
13714            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13715            .await
13716            .unwrap();
13717        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13718
13719        ctx.close().await;
13720    }
13721
13722    #[tokio::test(flavor = "multi_thread")]
13723    async fn test_sketch_on_plane_incremental() {
13724        let initial_source = "\
13725len = 2mm
13726cube = startSketchOn(XY)
13727  |> startProfile(at = [0, 0])
13728  |> line(end = [len, 0], tag = $side)
13729  |> line(end = [0, len])
13730  |> line(end = [-len, 0])
13731  |> line(end = [0, -len])
13732  |> close()
13733  |> extrude(length = len)
13734
13735plane = planeOf(cube, face = side)
13736";
13737
13738        let program = Program::parse(initial_source).unwrap().0.unwrap();
13739
13740        let mut frontend = FrontendState::new();
13741
13742        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13743        let mock_ctx = ExecutorContext::new_mock(None).await;
13744        let version = Version(0);
13745
13746        frontend.hack_set_program(&ctx, program).await.unwrap();
13747        // Find the last plane since the first plane is the XY plane.
13748        let plane_object = frontend
13749            .scene_graph
13750            .objects
13751            .iter()
13752            .rev()
13753            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13754            .unwrap();
13755        let plane_id = plane_object.id;
13756
13757        let sketch_args = SketchCtor {
13758            on: Plane::Object(plane_id),
13759        };
13760        let (src_delta, scene_delta, sketch_id) = frontend
13761            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13762            .await
13763            .unwrap();
13764        assert_eq!(
13765            src_delta.text.as_str(),
13766            "\
13767len = 2mm
13768cube = startSketchOn(XY)
13769  |> startProfile(at = [0, 0])
13770  |> line(end = [len, 0], tag = $side)
13771  |> line(end = [0, len])
13772  |> line(end = [-len, 0])
13773  |> line(end = [0, -len])
13774  |> close()
13775  |> extrude(length = len)
13776
13777plane = planeOf(cube, face = side)
13778sketch001 = sketch(on = plane) {
13779}
13780"
13781        );
13782        assert_eq!(sketch_id, ObjectId(2));
13783        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13784        let sketch_object = &scene_delta.new_graph.objects[2];
13785        assert_eq!(sketch_object.id, ObjectId(2));
13786        assert_eq!(
13787            sketch_object.kind,
13788            ObjectKind::Sketch(Sketch {
13789                args: SketchCtor {
13790                    on: Plane::Object(plane_id),
13791                },
13792                plane: plane_id,
13793                segments: vec![],
13794                constraints: vec![],
13795            })
13796        );
13797        assert_eq!(scene_delta.new_graph.objects.len(), 9);
13798
13799        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13800        assert_eq!(plane_object.id, plane_id);
13801        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13802
13803        ctx.close().await;
13804        mock_ctx.close().await;
13805    }
13806
13807    #[tokio::test(flavor = "multi_thread")]
13808    async fn test_new_sketch_uses_unique_variable_name() {
13809        let initial_source = "\
13810sketch1 = sketch(on = XY) {
13811}
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 version = Version(0);
13819
13820        frontend.hack_set_program(&ctx, program).await.unwrap();
13821
13822        let sketch_args = SketchCtor {
13823            on: Plane::Default(PlaneName::Yz),
13824        };
13825        let (src_delta, _, _) = frontend
13826            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13827            .await
13828            .unwrap();
13829
13830        assert_eq!(
13831            src_delta.text.as_str(),
13832            "\
13833sketch1 = sketch(on = XY) {
13834}
13835sketch001 = sketch(on = YZ) {
13836}
13837"
13838        );
13839
13840        ctx.close().await;
13841    }
13842
13843    #[tokio::test(flavor = "multi_thread")]
13844    async fn test_new_sketch_twice_using_same_plane() {
13845        let initial_source = "\
13846sketch1 = sketch(on = XY) {
13847}
13848";
13849
13850        let program = Program::parse(initial_source).unwrap().0.unwrap();
13851
13852        let mut frontend = FrontendState::new();
13853        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13854        let version = Version(0);
13855
13856        frontend.hack_set_program(&ctx, program).await.unwrap();
13857
13858        let sketch_args = SketchCtor {
13859            on: Plane::Default(PlaneName::Xy),
13860        };
13861        let (src_delta, _, _) = frontend
13862            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13863            .await
13864            .unwrap();
13865
13866        assert_eq!(
13867            src_delta.text.as_str(),
13868            "\
13869sketch1 = sketch(on = XY) {
13870}
13871sketch001 = sketch(on = XY) {
13872}
13873"
13874        );
13875
13876        ctx.close().await;
13877    }
13878
13879    #[tokio::test(flavor = "multi_thread")]
13880    async fn test_sketch_mode_reuses_cached_on_expression() {
13881        let initial_source = "\
13882width = 2mm
13883sketch(on = offsetPlane(XY, offset = width)) {
13884  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13885  distance([line1.start, line1.end]) == width
13886}
13887";
13888        let program = Program::parse(initial_source).unwrap().0.unwrap();
13889
13890        let mut frontend = FrontendState::new();
13891        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13892        let mock_ctx = ExecutorContext::new_mock(None).await;
13893        let version = Version(0);
13894        let project_id = ProjectId(0);
13895        let file_id = FileId(0);
13896
13897        frontend.hack_set_program(&ctx, program).await.unwrap();
13898        let initial_object_count = frontend.scene_graph.objects.len();
13899        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13900            .expect("Expected sketch object to exist")
13901            .id;
13902
13903        // Entering sketch mode should reuse cached `on` expression state
13904        // (offsetPlane result), not fail or create extra on-surface objects.
13905        let scene_delta = frontend
13906            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13907            .await
13908            .unwrap();
13909        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13910
13911        // A follow-up sketch-mode execution should keep the same stable object
13912        // graph shape as well.
13913        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13914        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13915
13916        ctx.close().await;
13917        mock_ctx.close().await;
13918    }
13919
13920    #[tokio::test(flavor = "multi_thread")]
13921    async fn test_multiple_sketch_blocks() {
13922        let initial_source = "\
13923// Cube that requires the engine.
13924width = 2
13925sketch001 = startSketchOn(XY)
13926profile001 = startProfile(sketch001, at = [0, 0])
13927  |> yLine(length = width, tag = $seg1)
13928  |> xLine(length = width)
13929  |> yLine(length = -width)
13930  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13931  |> close()
13932extrude001 = extrude(profile001, length = width)
13933
13934// Get a value that requires the engine.
13935x = segLen(seg1)
13936
13937// Triangle with side length 2*x.
13938sketch(on = XY) {
13939  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13940  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13941  coincident([line1.end, line2.start])
13942  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13943  coincident([line2.end, line3.start])
13944  coincident([line3.end, line1.start])
13945  equalLength([line3, line1])
13946  equalLength([line1, line2])
13947  distance([line1.start, line1.end]) == 2*x
13948}
13949
13950// Line segment with length x.
13951sketch2 = sketch(on = XY) {
13952  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13953  distance([line1.start, line1.end]) == x
13954}
13955";
13956
13957        let program = Program::parse(initial_source).unwrap().0.unwrap();
13958
13959        let mut frontend = FrontendState::new();
13960
13961        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13962        let mock_ctx = ExecutorContext::new_mock(None).await;
13963        let version = Version(0);
13964        let project_id = ProjectId(0);
13965        let file_id = FileId(0);
13966
13967        frontend.hack_set_program(&ctx, program).await.unwrap();
13968        let sketch_objects = frontend
13969            .scene_graph
13970            .objects
13971            .iter()
13972            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13973            .collect::<Vec<_>>();
13974        let sketch1_id = sketch_objects.first().unwrap().id;
13975        let sketch2_id = sketch_objects.get(1).unwrap().id;
13976        // First point in sketch1.
13977        let point1_id = ObjectId(sketch1_id.0 + 1);
13978        // First point in sketch2.
13979        let point2_id = ObjectId(sketch2_id.0 + 1);
13980
13981        // Edit the first sketch. Objects before the sketch block should be
13982        // present from execution cache so that we can sketch on prior planes,
13983        // for example. Objects after the first sketch block should not be
13984        // present since those statements are skipped in sketch mode.
13985        //
13986        // - startSketchOn(XY) Plane 1
13987        // - sketch on=XY Plane 1
13988        // - Sketch block 16
13989        let scene_delta = frontend
13990            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13991            .await
13992            .unwrap();
13993        assert_eq!(
13994            scene_delta.new_graph.objects.len(),
13995            18,
13996            "{:#?}",
13997            scene_delta.new_graph.objects
13998        );
13999
14000        // Edit a point in the first sketch.
14001        let point_ctor = PointCtor {
14002            position: Point2d {
14003                x: Expr::Var(Number {
14004                    value: 1.0,
14005                    units: NumericSuffix::Mm,
14006                }),
14007                y: Expr::Var(Number {
14008                    value: 2.0,
14009                    units: NumericSuffix::Mm,
14010                }),
14011            },
14012        };
14013        let segments = vec![ExistingSegmentCtor {
14014            id: point1_id,
14015            ctor: SegmentCtor::Point(point_ctor),
14016        }];
14017        let (src_delta, _) = frontend
14018            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14019            .await
14020            .unwrap();
14021        // Only the first sketch block changes.
14022        assert_eq!(
14023            src_delta.text.as_str(),
14024            "\
14025// Cube that requires the engine.
14026width = 2
14027sketch001 = startSketchOn(XY)
14028profile001 = startProfile(sketch001, at = [0, 0])
14029  |> yLine(length = width, tag = $seg1)
14030  |> xLine(length = width)
14031  |> yLine(length = -width)
14032  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14033  |> close()
14034extrude001 = extrude(profile001, length = width)
14035
14036// Get a value that requires the engine.
14037x = segLen(seg1)
14038
14039// Triangle with side length 2*x.
14040sketch(on = XY) {
14041  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14042  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14043  coincident([line1.end, line2.start])
14044  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14045  coincident([line2.end, line3.start])
14046  coincident([line3.end, line1.start])
14047  equalLength([line3, line1])
14048  equalLength([line1, line2])
14049  distance([line1.start, line1.end]) == 2 * x
14050}
14051
14052// Line segment with length x.
14053sketch2 = sketch(on = XY) {
14054  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14055  distance([line1.start, line1.end]) == x
14056}
14057"
14058        );
14059        let edited_sketch1_source = src_delta.text.clone();
14060
14061        // Execute mock to simulate drag end.
14062        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14063        assert_eq!(src_delta.text, edited_sketch1_source);
14064        // Exit sketch. Objects from the entire program should be present.
14065        //
14066        // - startSketchOn(XY) Plane 1
14067        // - sketch on=XY Plane 1
14068        // - Sketch block 16
14069        // - sketch on=XY cached
14070        // - Sketch block 5
14071        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14072        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14073
14074        // Edit the second sketch.
14075        //
14076        // - startSketchOn(XY) Plane 1
14077        // - sketch on=XY Plane 1
14078        // - Sketch block 16
14079        // - sketch on=XY cached
14080        // - Sketch block 5
14081        let scene_delta = frontend
14082            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14083            .await
14084            .unwrap();
14085        assert_eq!(
14086            scene_delta.new_graph.objects.len(),
14087            24,
14088            "{:#?}",
14089            scene_delta.new_graph.objects
14090        );
14091
14092        // Edit a point in the second sketch.
14093        let point_ctor = PointCtor {
14094            position: Point2d {
14095                x: Expr::Var(Number {
14096                    value: 3.0,
14097                    units: NumericSuffix::Mm,
14098                }),
14099                y: Expr::Var(Number {
14100                    value: 4.0,
14101                    units: NumericSuffix::Mm,
14102                }),
14103            },
14104        };
14105        let segments = vec![ExistingSegmentCtor {
14106            id: point2_id,
14107            ctor: SegmentCtor::Point(point_ctor),
14108        }];
14109        let (src_delta, _) = frontend
14110            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14111            .await
14112            .unwrap();
14113        // Only the second sketch block changes.
14114        assert_eq!(
14115            src_delta.text.as_str(),
14116            "\
14117// Cube that requires the engine.
14118width = 2
14119sketch001 = startSketchOn(XY)
14120profile001 = startProfile(sketch001, at = [0, 0])
14121  |> yLine(length = width, tag = $seg1)
14122  |> xLine(length = width)
14123  |> yLine(length = -width)
14124  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14125  |> close()
14126extrude001 = extrude(profile001, length = width)
14127
14128// Get a value that requires the engine.
14129x = segLen(seg1)
14130
14131// Triangle with side length 2*x.
14132sketch(on = XY) {
14133  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14134  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14135  coincident([line1.end, line2.start])
14136  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14137  coincident([line2.end, line3.start])
14138  coincident([line3.end, line1.start])
14139  equalLength([line3, line1])
14140  equalLength([line1, line2])
14141  distance([line1.start, line1.end]) == 2 * x
14142}
14143
14144// Line segment with length x.
14145sketch2 = sketch(on = XY) {
14146  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14147  distance([line1.start, line1.end]) == x
14148}
14149"
14150        );
14151        let edited_sketch2_source = src_delta.text.clone();
14152
14153        // Execute mock to simulate drag end.
14154        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14155        assert_eq!(src_delta.text, edited_sketch2_source);
14156
14157        ctx.close().await;
14158        mock_ctx.close().await;
14159    }
14160
14161    #[tokio::test(flavor = "multi_thread")]
14162    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14163        clear_mem_cache().await;
14164
14165        let source = r#"sketch001 = sketch(on = XZ) {
14166  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14167}
14168sketch002 = sketch(on = XY) {
14169  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14170  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14171  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14172  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14173  coincident([line1.end, line2.start])
14174  coincident([line2.end, line3.start])
14175  coincident([line3.end, line4.start])
14176  coincident([line4.end, line1.start])
14177  parallel([line2, line4])
14178  parallel([line3, line1])
14179  perpendicular([line1, line2])
14180  horizontal(line3)
14181  coincident([line1.start, ORIGIN])
14182}
14183"#;
14184
14185        let program = Program::parse(source).unwrap().0.unwrap();
14186        let mut frontend = FrontendState::new();
14187        let ctx = ExecutorContext::new_with_engine(
14188            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
14189            Default::default(),
14190        );
14191        let mock_ctx = ExecutorContext::new_mock(None).await;
14192        let version = Version(0);
14193        let project_id = ProjectId(0);
14194        let file_id = FileId(0);
14195
14196        frontend.hack_set_program(&ctx, program).await.unwrap();
14197        let sketch_objects = frontend
14198            .scene_graph
14199            .objects
14200            .iter()
14201            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14202            .collect::<Vec<_>>();
14203        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14204
14205        let sketch1_id = sketch_objects[0].id;
14206        let sketch2_id = sketch_objects[1].id;
14207
14208        frontend
14209            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14210            .await
14211            .unwrap();
14212        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14213
14214        let scene_delta = frontend
14215            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14216            .await
14217            .unwrap();
14218        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14219
14220        clear_mem_cache().await;
14221        ctx.close().await;
14222        mock_ctx.close().await;
14223    }
14224
14225    // Regression tests: operations on source code with extra whitespace/newlines.
14226    // These test that NodePath-based lookups work correctly when source ranges
14227    // are shifted by extra whitespace that wouldn't be present after formatting.
14228
14229    #[tokio::test(flavor = "multi_thread")]
14230    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14231        // Extra newlines after @settings line - this shifts all source ranges.
14232        let initial_source = "@settings(defaultLengthUnit = mm)
14233
14234
14235
14236sketch001 = sketch(on = XY) {
14237  point(at = [1in, 2in])
14238}
14239";
14240
14241        let program = Program::parse(initial_source).unwrap().0.unwrap();
14242        let mut frontend = FrontendState::new();
14243
14244        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14245        let mock_ctx = ExecutorContext::new_mock(None).await;
14246        let version = Version(0);
14247        let project_id = ProjectId(0);
14248        let file_id = FileId(0);
14249
14250        frontend.hack_set_program(&ctx, program).await.unwrap();
14251        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14252        let sketch_id = sketch_object.id;
14253
14254        // Edit sketch should succeed despite extra newlines.
14255        frontend
14256            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14257            .await
14258            .unwrap();
14259
14260        // Add a new point to the sketch.
14261        let point_ctor = PointCtor {
14262            position: Point2d {
14263                x: Expr::Number(Number {
14264                    value: 5.0,
14265                    units: NumericSuffix::Mm,
14266                }),
14267                y: Expr::Number(Number {
14268                    value: 6.0,
14269                    units: NumericSuffix::Mm,
14270                }),
14271            },
14272        };
14273        let segment = SegmentCtor::Point(point_ctor);
14274        let (src_delta, scene_delta) = frontend
14275            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14276            .await
14277            .unwrap();
14278        // After adding a point, the source should be reformatted with standard whitespace.
14279        assert!(
14280            src_delta.text.contains("point(at = [5mm, 6mm])"),
14281            "Expected new point in source, got: {}",
14282            src_delta.text
14283        );
14284        assert!(!scene_delta.new_objects.is_empty());
14285
14286        ctx.close().await;
14287        mock_ctx.close().await;
14288    }
14289
14290    #[tokio::test(flavor = "multi_thread")]
14291    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14292        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14293
14294        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14295        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14296
14297        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14298        assert!(
14299            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14300            "Expected experimental settings to be added to source"
14301        );
14302    }
14303
14304    #[tokio::test(flavor = "multi_thread")]
14305    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14306        // Extra newlines after @settings, with an empty sketch block.
14307        let initial_source = "@settings(defaultLengthUnit = mm)
14308
14309
14310
14311s = sketch(on = XY) {}
14312";
14313
14314        let program = Program::parse(initial_source).unwrap().0.unwrap();
14315        let mut frontend = FrontendState::new();
14316
14317        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14318        let mock_ctx = ExecutorContext::new_mock(None).await;
14319        let version = Version(0);
14320
14321        frontend.hack_set_program(&ctx, program).await.unwrap();
14322        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14323        let sketch_id = sketch_object.id;
14324
14325        let line_ctor = LineCtor {
14326            start: Point2d {
14327                x: Expr::Number(Number {
14328                    value: 0.0,
14329                    units: NumericSuffix::Mm,
14330                }),
14331                y: Expr::Number(Number {
14332                    value: 0.0,
14333                    units: NumericSuffix::Mm,
14334                }),
14335            },
14336            end: Point2d {
14337                x: Expr::Number(Number {
14338                    value: 10.0,
14339                    units: NumericSuffix::Mm,
14340                }),
14341                y: Expr::Number(Number {
14342                    value: 10.0,
14343                    units: NumericSuffix::Mm,
14344                }),
14345            },
14346            construction: None,
14347        };
14348        let segment = SegmentCtor::Line(line_ctor);
14349        let (src_delta, scene_delta) = frontend
14350            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14351            .await
14352            .unwrap();
14353        assert!(
14354            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14355            "Expected line in source, got: {}",
14356            src_delta.text
14357        );
14358        // Line creates start point, end point, and line segment.
14359        assert_eq!(scene_delta.new_objects.len(), 3);
14360
14361        ctx.close().await;
14362        mock_ctx.close().await;
14363    }
14364
14365    #[tokio::test(flavor = "multi_thread")]
14366    async fn test_extra_newlines_between_operations_edit_line() {
14367        // Extra newlines between @settings and sketch, and inside the sketch block.
14368        let initial_source = "@settings(defaultLengthUnit = mm)
14369
14370
14371sketch001 = sketch(on = XY) {
14372
14373  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14374
14375}
14376";
14377
14378        let program = Program::parse(initial_source).unwrap().0.unwrap();
14379        let mut frontend = FrontendState::new();
14380
14381        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14382        let mock_ctx = ExecutorContext::new_mock(None).await;
14383        let version = Version(0);
14384        let project_id = ProjectId(0);
14385        let file_id = FileId(0);
14386
14387        frontend.hack_set_program(&ctx, program).await.unwrap();
14388        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14389        let sketch_id = sketch_object.id;
14390        let sketch = expect_sketch(sketch_object);
14391
14392        // Extract segment IDs before edit_sketch borrows frontend mutably.
14393        let line_id = sketch
14394            .segments
14395            .iter()
14396            .copied()
14397            .find(|seg_id| {
14398                matches!(
14399                    &frontend.scene_graph.objects[seg_id.0].kind,
14400                    ObjectKind::Segment {
14401                        segment: Segment::Line(_)
14402                    }
14403                )
14404            })
14405            .expect("Expected a line segment in sketch");
14406
14407        // Enter sketch edit mode.
14408        frontend
14409            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14410            .await
14411            .unwrap();
14412
14413        // Edit the line.
14414        let line_ctor = LineCtor {
14415            start: Point2d {
14416                x: Expr::Var(Number {
14417                    value: 1.0,
14418                    units: NumericSuffix::Mm,
14419                }),
14420                y: Expr::Var(Number {
14421                    value: 2.0,
14422                    units: NumericSuffix::Mm,
14423                }),
14424            },
14425            end: Point2d {
14426                x: Expr::Var(Number {
14427                    value: 13.0,
14428                    units: NumericSuffix::Mm,
14429                }),
14430                y: Expr::Var(Number {
14431                    value: 14.0,
14432                    units: NumericSuffix::Mm,
14433                }),
14434            },
14435            construction: None,
14436        };
14437        let segments = vec![ExistingSegmentCtor {
14438            id: line_id,
14439            ctor: SegmentCtor::Line(line_ctor),
14440        }];
14441        let (src_delta, _scene_delta) = frontend
14442            .edit_segments(&mock_ctx, version, sketch_id, segments)
14443            .await
14444            .unwrap();
14445        assert!(
14446            src_delta
14447                .text
14448                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14449            "Expected edited line in source, got: {}",
14450            src_delta.text
14451        );
14452
14453        ctx.close().await;
14454        mock_ctx.close().await;
14455    }
14456
14457    #[tokio::test(flavor = "multi_thread")]
14458    async fn test_extra_newlines_delete_segment() {
14459        // Extra whitespace before and after the sketch block.
14460        let initial_source = "@settings(defaultLengthUnit = mm)
14461
14462
14463
14464sketch001 = sketch(on = XY) {
14465  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14466}
14467";
14468
14469        let program = Program::parse(initial_source).unwrap().0.unwrap();
14470        let mut frontend = FrontendState::new();
14471
14472        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14473        let mock_ctx = ExecutorContext::new_mock(None).await;
14474        let version = Version(0);
14475
14476        frontend.hack_set_program(&ctx, program).await.unwrap();
14477        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14478        let sketch_id = sketch_object.id;
14479        let sketch = expect_sketch(sketch_object);
14480
14481        // The sketch should have 3 segments: start point, center point, and the circle.
14482        assert_eq!(sketch.segments.len(), 3);
14483        let circle_id = sketch.segments[2];
14484
14485        // Delete the circle despite extra newlines in original source.
14486        let (src_delta, scene_delta) = frontend
14487            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14488            .await
14489            .unwrap();
14490        assert!(
14491            src_delta.text.contains("sketch(on = XY) {"),
14492            "Expected sketch block in source, got: {}",
14493            src_delta.text
14494        );
14495        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14496        let new_sketch = expect_sketch(new_sketch_object);
14497        assert_eq!(new_sketch.segments.len(), 0);
14498
14499        ctx.close().await;
14500        mock_ctx.close().await;
14501    }
14502
14503    #[tokio::test(flavor = "multi_thread")]
14504    async fn test_unformatted_source_add_arc() {
14505        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14506        let initial_source = "@settings(defaultLengthUnit = mm)
14507
14508
14509
14510
14511sketch001 = sketch(on = XY) {
14512}
14513";
14514
14515        let program = Program::parse(initial_source).unwrap().0.unwrap();
14516        let mut frontend = FrontendState::new();
14517
14518        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14519        let mock_ctx = ExecutorContext::new_mock(None).await;
14520        let version = Version(0);
14521
14522        frontend.hack_set_program(&ctx, program).await.unwrap();
14523        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14524        let sketch_id = sketch_object.id;
14525
14526        let arc_ctor = ArcCtor {
14527            start: Point2d {
14528                x: Expr::Var(Number {
14529                    value: 5.0,
14530                    units: NumericSuffix::Mm,
14531                }),
14532                y: Expr::Var(Number {
14533                    value: 0.0,
14534                    units: NumericSuffix::Mm,
14535                }),
14536            },
14537            end: Point2d {
14538                x: Expr::Var(Number {
14539                    value: 0.0,
14540                    units: NumericSuffix::Mm,
14541                }),
14542                y: Expr::Var(Number {
14543                    value: 5.0,
14544                    units: NumericSuffix::Mm,
14545                }),
14546            },
14547            center: Point2d {
14548                x: Expr::Var(Number {
14549                    value: 0.0,
14550                    units: NumericSuffix::Mm,
14551                }),
14552                y: Expr::Var(Number {
14553                    value: 0.0,
14554                    units: NumericSuffix::Mm,
14555                }),
14556            },
14557            construction: None,
14558        };
14559        let segment = SegmentCtor::Arc(arc_ctor);
14560        let (src_delta, scene_delta) = frontend
14561            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14562            .await
14563            .unwrap();
14564        assert!(
14565            src_delta
14566                .text
14567                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14568            "Expected arc in source, got: {}",
14569            src_delta.text
14570        );
14571        assert!(!scene_delta.new_objects.is_empty());
14572
14573        ctx.close().await;
14574        mock_ctx.close().await;
14575    }
14576
14577    #[tokio::test(flavor = "multi_thread")]
14578    async fn test_extra_newlines_add_circle() {
14579        // Extra blank lines between settings and sketch.
14580        let initial_source = "@settings(defaultLengthUnit = mm)
14581
14582
14583
14584sketch001 = sketch(on = XY) {
14585}
14586";
14587
14588        let program = Program::parse(initial_source).unwrap().0.unwrap();
14589        let mut frontend = FrontendState::new();
14590
14591        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14592        let mock_ctx = ExecutorContext::new_mock(None).await;
14593        let version = Version(0);
14594
14595        frontend.hack_set_program(&ctx, program).await.unwrap();
14596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14597        let sketch_id = sketch_object.id;
14598
14599        let circle_ctor = CircleCtor {
14600            start: Point2d {
14601                x: Expr::Var(Number {
14602                    value: 5.0,
14603                    units: NumericSuffix::Mm,
14604                }),
14605                y: Expr::Var(Number {
14606                    value: 0.0,
14607                    units: NumericSuffix::Mm,
14608                }),
14609            },
14610            center: Point2d {
14611                x: Expr::Var(Number {
14612                    value: 0.0,
14613                    units: NumericSuffix::Mm,
14614                }),
14615                y: Expr::Var(Number {
14616                    value: 0.0,
14617                    units: NumericSuffix::Mm,
14618                }),
14619            },
14620            construction: None,
14621        };
14622        let segment = SegmentCtor::Circle(circle_ctor);
14623        let (src_delta, scene_delta) = frontend
14624            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14625            .await
14626            .unwrap();
14627        assert!(
14628            src_delta
14629                .text
14630                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14631            "Expected circle in source, got: {}",
14632            src_delta.text
14633        );
14634        assert!(!scene_delta.new_objects.is_empty());
14635
14636        ctx.close().await;
14637        mock_ctx.close().await;
14638    }
14639
14640    #[tokio::test(flavor = "multi_thread")]
14641    async fn test_extra_newlines_add_constraint() {
14642        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14643        let initial_source = "@settings(defaultLengthUnit = mm)
14644
14645
14646
14647sketch001 = sketch(on = XY) {
14648  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14649  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14650}
14651";
14652
14653        let program = Program::parse(initial_source).unwrap().0.unwrap();
14654        let mut frontend = FrontendState::new();
14655
14656        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14657        let mock_ctx = ExecutorContext::new_mock(None).await;
14658        let version = Version(0);
14659        let project_id = ProjectId(0);
14660        let file_id = FileId(0);
14661
14662        frontend.hack_set_program(&ctx, program).await.unwrap();
14663        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14664        let sketch_id = sketch_object.id;
14665        let sketch = expect_sketch(sketch_object);
14666
14667        // Extract segment data before edit_sketch borrows frontend mutably.
14668        let line_ids: Vec<ObjectId> = sketch
14669            .segments
14670            .iter()
14671            .copied()
14672            .filter(|seg_id| {
14673                matches!(
14674                    &frontend.scene_graph.objects[seg_id.0].kind,
14675                    ObjectKind::Segment {
14676                        segment: Segment::Line(_)
14677                    }
14678                )
14679            })
14680            .collect();
14681        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14682
14683        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14684        let ObjectKind::Segment {
14685            segment: Segment::Line(line1_data),
14686        } = &line1.kind
14687        else {
14688            panic!("Expected line");
14689        };
14690        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14691        let ObjectKind::Segment {
14692            segment: Segment::Line(line2_data),
14693        } = &line2.kind
14694        else {
14695            panic!("Expected line");
14696        };
14697
14698        // Build constraint before entering sketch mode.
14699        let constraint = Constraint::Coincident(Coincident {
14700            segments: vec![line1_data.end.into(), line2_data.start.into()],
14701        });
14702
14703        // Enter sketch edit mode.
14704        frontend
14705            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14706            .await
14707            .unwrap();
14708        let (src_delta, _scene_delta) = frontend
14709            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14710            .await
14711            .unwrap();
14712        assert!(
14713            src_delta.text.contains("coincident("),
14714            "Expected coincident constraint in source, got: {}",
14715            src_delta.text
14716        );
14717
14718        ctx.close().await;
14719        mock_ctx.close().await;
14720    }
14721
14722    #[tokio::test(flavor = "multi_thread")]
14723    async fn test_extra_newlines_add_line_then_edit_line() {
14724        // Extra newlines after @settings - add a line, then edit it.
14725        let initial_source = "@settings(defaultLengthUnit = mm)
14726
14727
14728
14729sketch001 = sketch(on = XY) {
14730}
14731";
14732
14733        let program = Program::parse(initial_source).unwrap().0.unwrap();
14734        let mut frontend = FrontendState::new();
14735
14736        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14737        let mock_ctx = ExecutorContext::new_mock(None).await;
14738        let version = Version(0);
14739
14740        frontend.hack_set_program(&ctx, program).await.unwrap();
14741        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14742        let sketch_id = sketch_object.id;
14743
14744        // Add a line.
14745        let line_ctor = LineCtor {
14746            start: Point2d {
14747                x: Expr::Number(Number {
14748                    value: 0.0,
14749                    units: NumericSuffix::Mm,
14750                }),
14751                y: Expr::Number(Number {
14752                    value: 0.0,
14753                    units: NumericSuffix::Mm,
14754                }),
14755            },
14756            end: Point2d {
14757                x: Expr::Number(Number {
14758                    value: 10.0,
14759                    units: NumericSuffix::Mm,
14760                }),
14761                y: Expr::Number(Number {
14762                    value: 10.0,
14763                    units: NumericSuffix::Mm,
14764                }),
14765            },
14766            construction: None,
14767        };
14768        let segment = SegmentCtor::Line(line_ctor);
14769        let (src_delta, scene_delta) = frontend
14770            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14771            .await
14772            .unwrap();
14773        assert!(
14774            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14775            "Expected line in source after add, got: {}",
14776            src_delta.text
14777        );
14778        // Line creates start point, end point, and line segment.
14779        let line_id = *scene_delta.new_objects.last().unwrap();
14780
14781        // Edit the line.
14782        let line_ctor = LineCtor {
14783            start: Point2d {
14784                x: Expr::Number(Number {
14785                    value: 1.0,
14786                    units: NumericSuffix::Mm,
14787                }),
14788                y: Expr::Number(Number {
14789                    value: 2.0,
14790                    units: NumericSuffix::Mm,
14791                }),
14792            },
14793            end: Point2d {
14794                x: Expr::Number(Number {
14795                    value: 13.0,
14796                    units: NumericSuffix::Mm,
14797                }),
14798                y: Expr::Number(Number {
14799                    value: 14.0,
14800                    units: NumericSuffix::Mm,
14801                }),
14802            },
14803            construction: None,
14804        };
14805        let segments = vec![ExistingSegmentCtor {
14806            id: line_id,
14807            ctor: SegmentCtor::Line(line_ctor),
14808        }];
14809        let (src_delta, scene_delta) = frontend
14810            .edit_segments(&mock_ctx, version, sketch_id, segments)
14811            .await
14812            .unwrap();
14813        assert!(
14814            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14815            "Expected edited line in source, got: {}",
14816            src_delta.text
14817        );
14818        assert_eq!(scene_delta.new_objects, vec![]);
14819
14820        ctx.close().await;
14821        mock_ctx.close().await;
14822    }
14823}