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
2560        self.program = new_program.clone();
2561
2562        let mut truncated_program = new_program;
2563        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2564            .map_err(KclErrorWithOutputs::no_outputs)?;
2565
2566        let outcome = ctx
2567            .run_mock(
2568                &truncated_program,
2569                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2570            )
2571            .await?;
2572
2573        let new_object_ids = {
2574            let make_err =
2575                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2576            let segment_id = outcome
2577                .source_range_to_object
2578                .get(&spline_node_ref.range)
2579                .copied()
2580                .ok_or_else(|| {
2581                    make_err(format!(
2582                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2583                    ))
2584                })?;
2585            let segment_object = outcome
2586                .scene_objects
2587                .get(segment_id.0)
2588                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2589            let ObjectKind::Segment { segment } = &segment_object.kind else {
2590                return Err(make_err(format!(
2591                    "Object is not a segment, it is {}",
2592                    segment_object.kind.human_friendly_kind_with_article()
2593                )));
2594            };
2595            let Segment::ControlPointSpline(spline) = segment else {
2596                return Err(make_err(format!(
2597                    "Segment is not a control point spline, it is {}",
2598                    segment.human_friendly_kind_with_article()
2599                )));
2600            };
2601
2602            let mut ids = outcome
2603                .scene_objects
2604                .iter()
2605                .filter_map(|obj| match &obj.kind {
2606                    ObjectKind::Segment {
2607                        segment: Segment::Line(line),
2608                    } if line.owner == Some(segment_id) => Some(obj.id),
2609                    _ => None,
2610                })
2611                .collect::<Vec<_>>();
2612            ids.extend(spline.controls.clone());
2613            ids.push(segment_id);
2614            ids
2615        };
2616        let src_delta = SourceDelta { text: new_source };
2617        let outcome = self.update_state_after_exec(outcome, false);
2618        let scene_graph_delta = SceneGraphDelta {
2619            new_graph: self.scene_graph_for_ui(),
2620            invalidates_ids: false,
2621            new_objects: new_object_ids,
2622            exec_outcome: outcome,
2623        };
2624        Ok((src_delta, scene_graph_delta))
2625    }
2626
2627    fn edit_point(
2628        &mut self,
2629        new_ast: &mut ast::Node<ast::Program>,
2630        sketch: ObjectId,
2631        point: ObjectId,
2632        ctor: PointCtor,
2633    ) -> Result<(), KclError> {
2634        // Create updated KCL source from args.
2635        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2636
2637        // Look up existing sketch.
2638        let sketch_id = sketch;
2639        let sketch_object = self
2640            .scene_graph
2641            .objects
2642            .get(sketch_id.0)
2643            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2644        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2645            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2646        };
2647        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2648            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2649        })?;
2650        // Look up existing point.
2651        let point_id = point;
2652        let point_object = self
2653            .scene_graph
2654            .objects
2655            .get(point_id.0)
2656            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2657        let ObjectKind::Segment {
2658            segment: Segment::Point(point),
2659        } = &point_object.kind
2660        else {
2661            return Err(KclError::refactor(format!(
2662                "Object is not a point segment: {point_object:?}"
2663            )));
2664        };
2665
2666        // If the point is part of a line or arc, edit the line/arc instead.
2667        if let Some(owner_id) = point.owner {
2668            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2669                KclError::refactor(format!(
2670                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2671                ))
2672            })?;
2673            let ObjectKind::Segment { segment } = &owner_object.kind else {
2674                return Err(KclError::refactor(format!(
2675                    "Internal: Owner of point is not a segment, but found {}",
2676                    owner_object.kind.human_friendly_kind_with_article()
2677                )));
2678            };
2679
2680            // Handle Line owner
2681            if let Segment::Line(line) = segment {
2682                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2683                    return Err(KclError::refactor(format!(
2684                        "Internal: Owner of point does not have line ctor, but found {}",
2685                        line.ctor.human_friendly_kind_with_article()
2686                    )));
2687                };
2688                let mut line_ctor = line_ctor.clone();
2689                // Which end of the line is this point?
2690                if line.start == point_id {
2691                    line_ctor.start = ctor.position;
2692                } else if line.end == point_id {
2693                    line_ctor.end = ctor.position;
2694                } else {
2695                    return Err(KclError::refactor(format!(
2696                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2697                    )));
2698                }
2699                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2700            }
2701
2702            // Handle Arc owner
2703            if let Segment::Arc(arc) = segment {
2704                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2705                    return Err(KclError::refactor(format!(
2706                        "Internal: Owner of point does not have arc ctor, but found {}",
2707                        arc.ctor.human_friendly_kind_with_article()
2708                    )));
2709                };
2710                let mut arc_ctor = arc_ctor.clone();
2711                // Which point of the arc is this? (center, start, or end)
2712                if arc.center == point_id {
2713                    arc_ctor.center = ctor.position;
2714                } else if arc.start == point_id {
2715                    arc_ctor.start = ctor.position;
2716                } else if arc.end == point_id {
2717                    arc_ctor.end = ctor.position;
2718                } else {
2719                    return Err(KclError::refactor(format!(
2720                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2721                    )));
2722                }
2723                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2724            }
2725
2726            // Handle Circle owner
2727            if let Segment::Circle(circle) = segment {
2728                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2729                    return Err(KclError::refactor(format!(
2730                        "Internal: Owner of point does not have circle ctor, but found {}",
2731                        circle.ctor.human_friendly_kind_with_article()
2732                    )));
2733                };
2734                let mut circle_ctor = circle_ctor.clone();
2735                if circle.center == point_id {
2736                    circle_ctor.center = ctor.position;
2737                } else if circle.start == point_id {
2738                    circle_ctor.start = ctor.position;
2739                } else {
2740                    return Err(KclError::refactor(format!(
2741                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2742                    )));
2743                }
2744                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2745            }
2746
2747            if let Segment::ControlPointSpline(spline) = segment {
2748                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2749                    return Err(KclError::refactor(format!(
2750                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2751                        spline.ctor.human_friendly_kind_with_article()
2752                    )));
2753                };
2754                let mut spline_ctor = spline_ctor.clone();
2755                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2756                    return Err(KclError::refactor(format!(
2757                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2758                    )));
2759                };
2760                spline_ctor.points[control_index] = ctor.position;
2761                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2762            }
2763
2764            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2765            // (fall through to the point editing logic below)
2766        }
2767
2768        // Modify the point AST.
2769        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2770        Ok(())
2771    }
2772
2773    fn edit_line(
2774        &mut self,
2775        new_ast: &mut ast::Node<ast::Program>,
2776        sketch: ObjectId,
2777        line: ObjectId,
2778        ctor: LineCtor,
2779    ) -> Result<(), KclError> {
2780        // Create updated KCL source from args.
2781        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2782        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2783
2784        // Look up existing sketch.
2785        let sketch_id = sketch;
2786        let sketch_object = self
2787            .scene_graph
2788            .objects
2789            .get(sketch_id.0)
2790            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2791        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2792            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2793        };
2794        sketch
2795            .segments
2796            .iter()
2797            .find(|o| **o == line)
2798            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2799        // Look up existing line.
2800        let line_id = line;
2801        let line_object = self
2802            .scene_graph
2803            .objects
2804            .get(line_id.0)
2805            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2806        let ObjectKind::Segment { .. } = &line_object.kind else {
2807            let kind = line_object.kind.human_friendly_kind_with_article();
2808            return Err(KclError::refactor(format!(
2809                "This constraint only works on Segments, but you selected {kind}"
2810            )));
2811        };
2812
2813        // Modify the line AST.
2814        self.mutate_ast(
2815            new_ast,
2816            line_id,
2817            AstMutateCommand::EditLine {
2818                start: new_start_ast,
2819                end: new_end_ast,
2820                construction: ctor.construction,
2821            },
2822        )?;
2823        Ok(())
2824    }
2825
2826    fn edit_arc(
2827        &mut self,
2828        new_ast: &mut ast::Node<ast::Program>,
2829        sketch: ObjectId,
2830        arc: ObjectId,
2831        ctor: ArcCtor,
2832    ) -> Result<(), KclError> {
2833        // Create updated KCL source from args.
2834        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2835        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2836        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2837
2838        // Look up existing sketch.
2839        let sketch_id = sketch;
2840        let sketch_object = self
2841            .scene_graph
2842            .objects
2843            .get(sketch_id.0)
2844            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2845        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2846            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2847        };
2848        sketch
2849            .segments
2850            .iter()
2851            .find(|o| **o == arc)
2852            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2853        // Look up existing arc.
2854        let arc_id = arc;
2855        let arc_object = self
2856            .scene_graph
2857            .objects
2858            .get(arc_id.0)
2859            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2860        let ObjectKind::Segment { .. } = &arc_object.kind else {
2861            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2862        };
2863
2864        // Modify the arc AST.
2865        self.mutate_ast(
2866            new_ast,
2867            arc_id,
2868            AstMutateCommand::EditArc {
2869                start: new_start_ast,
2870                end: new_end_ast,
2871                center: new_center_ast,
2872                construction: ctor.construction,
2873            },
2874        )?;
2875        Ok(())
2876    }
2877
2878    fn edit_circle(
2879        &mut self,
2880        new_ast: &mut ast::Node<ast::Program>,
2881        sketch: ObjectId,
2882        circle: ObjectId,
2883        ctor: CircleCtor,
2884    ) -> Result<(), KclError> {
2885        // Create updated KCL source from args.
2886        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2887        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2888
2889        // Look up existing sketch.
2890        let sketch_id = sketch;
2891        let sketch_object = self
2892            .scene_graph
2893            .objects
2894            .get(sketch_id.0)
2895            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2896        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2897            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2898        };
2899        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2900            KclError::refactor(format!(
2901                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2902            ))
2903        })?;
2904        // Look up existing circle.
2905        let circle_id = circle;
2906        let circle_object = self
2907            .scene_graph
2908            .objects
2909            .get(circle_id.0)
2910            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2911        let ObjectKind::Segment { .. } = &circle_object.kind else {
2912            return Err(KclError::refactor(format!(
2913                "Object is not a segment: {circle_object:?}"
2914            )));
2915        };
2916
2917        // Modify the circle AST.
2918        self.mutate_ast(
2919            new_ast,
2920            circle_id,
2921            AstMutateCommand::EditCircle {
2922                start: new_start_ast,
2923                center: new_center_ast,
2924                construction: ctor.construction,
2925            },
2926        )?;
2927        Ok(())
2928    }
2929
2930    fn edit_control_point_spline(
2931        &mut self,
2932        new_ast: &mut ast::Node<ast::Program>,
2933        sketch: ObjectId,
2934        spline: ObjectId,
2935        ctor: ControlPointSplineCtor,
2936    ) -> Result<(), KclError> {
2937        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2938
2939        let sketch_object = self
2940            .scene_graph
2941            .objects
2942            .get(sketch.0)
2943            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2944        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2945            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2946        };
2947        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2948            KclError::refactor(format!(
2949                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2950            ))
2951        })?;
2952
2953        let spline_object =
2954            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2955                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2956            })?;
2957        let ObjectKind::Segment { .. } = &spline_object.kind else {
2958            return Err(KclError::refactor(format!(
2959                "Object is not a segment: {spline_object:?}"
2960            )));
2961        };
2962
2963        self.mutate_ast(
2964            new_ast,
2965            spline,
2966            AstMutateCommand::EditControlPointSpline {
2967                points: points_ast,
2968                construction: ctor.construction,
2969            },
2970        )?;
2971        Ok(())
2972    }
2973
2974    fn delete_segment(
2975        &mut self,
2976        new_ast: &mut ast::Node<ast::Program>,
2977        sketch: ObjectId,
2978        segment_id: ObjectId,
2979    ) -> Result<(), KclError> {
2980        // Look up existing sketch.
2981        let sketch_id = sketch;
2982        let sketch_object = self
2983            .scene_graph
2984            .objects
2985            .get(sketch_id.0)
2986            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2987        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2988            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2989        };
2990        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2991            KclError::refactor(format!(
2992                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2993            ))
2994        })?;
2995        // Look up existing segment.
2996        let segment_object =
2997            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2998                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2999            })?;
3000        let ObjectKind::Segment { .. } = &segment_object.kind else {
3001            return Err(KclError::refactor(format!(
3002                "Object is not a segment, it is {}",
3003                segment_object.kind.human_friendly_kind_with_article()
3004            )));
3005        };
3006
3007        // Modify the AST to remove the segment.
3008        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3009        Ok(())
3010    }
3011
3012    fn delete_constraint(
3013        &mut self,
3014        new_ast: &mut ast::Node<ast::Program>,
3015        sketch: ObjectId,
3016        constraint_id: ObjectId,
3017    ) -> Result<(), KclError> {
3018        // Look up existing sketch.
3019        let sketch_id = sketch;
3020        let sketch_object = self
3021            .scene_graph
3022            .objects
3023            .get(sketch_id.0)
3024            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3025        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3026            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3027        };
3028        sketch
3029            .constraints
3030            .iter()
3031            .find(|o| **o == constraint_id)
3032            .ok_or_else(|| {
3033                KclError::refactor(format!(
3034                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3035                ))
3036            })?;
3037        // Look up existing constraint.
3038        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3039            KclError::refactor(format!(
3040                "Constraint not found in scene graph: constraint={constraint_id:?}"
3041            ))
3042        })?;
3043        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3044            return Err(KclError::refactor(format!(
3045                "Object is not a constraint, it is {}",
3046                constraint_object.kind.human_friendly_kind_with_article()
3047            )));
3048        };
3049
3050        // Modify the AST to remove the constraint.
3051        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3052        Ok(())
3053    }
3054
3055    fn edit_coincident_constraint(
3056        &mut self,
3057        new_ast: &mut ast::Node<ast::Program>,
3058        constraint_id: ObjectId,
3059        segments: Vec<ConstraintSegment>,
3060    ) -> Result<(), KclError> {
3061        if segments.len() < 2 {
3062            return Err(KclError::refactor(format!(
3063                "Coincident constraint must have at least 2 inputs, got {}",
3064                segments.len()
3065            )));
3066        }
3067
3068        let segment_asts = segments
3069            .iter()
3070            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3071            .collect::<Result<Vec<_>, _>>()?;
3072
3073        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3074            elements: segment_asts,
3075            digest: None,
3076            non_code_meta: Default::default(),
3077        })));
3078
3079        self.mutate_ast(
3080            new_ast,
3081            constraint_id,
3082            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3083        )?;
3084        Ok(())
3085    }
3086
3087    fn edit_horizontal_points_constraint(
3088        &mut self,
3089        new_ast: &mut ast::Node<ast::Program>,
3090        constraint_id: ObjectId,
3091        points: Vec<ConstraintSegment>,
3092    ) -> Result<(), KclError> {
3093        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3094    }
3095
3096    fn edit_vertical_points_constraint(
3097        &mut self,
3098        new_ast: &mut ast::Node<ast::Program>,
3099        constraint_id: ObjectId,
3100        points: Vec<ConstraintSegment>,
3101    ) -> Result<(), KclError> {
3102        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3103    }
3104
3105    fn edit_axis_points_constraint(
3106        &mut self,
3107        new_ast: &mut ast::Node<ast::Program>,
3108        constraint_id: ObjectId,
3109        points: Vec<ConstraintSegment>,
3110        constraint_name: &str,
3111    ) -> Result<(), KclError> {
3112        if points.len() < 2 {
3113            return Err(KclError::refactor(format!(
3114                "{constraint_name} points constraint must have at least 2 points, got {}",
3115                points.len()
3116            )));
3117        }
3118
3119        let point_asts = points
3120            .iter()
3121            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3122            .collect::<Result<Vec<_>, _>>()?;
3123
3124        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3125            elements: point_asts,
3126            digest: None,
3127            non_code_meta: Default::default(),
3128        })));
3129
3130        self.mutate_ast(
3131            new_ast,
3132            constraint_id,
3133            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3134        )?;
3135        Ok(())
3136    }
3137
3138    /// updates the equalLength constraint with the given lines
3139    fn edit_equal_length_constraint(
3140        &mut self,
3141        new_ast: &mut ast::Node<ast::Program>,
3142        constraint_id: ObjectId,
3143        lines: Vec<ObjectId>,
3144    ) -> Result<(), KclError> {
3145        if lines.len() < 2 {
3146            return Err(KclError::refactor(format!(
3147                "Lines equal length constraint must have at least 2 lines, got {}",
3148                lines.len()
3149            )));
3150        }
3151
3152        let line_asts = lines
3153            .iter()
3154            .map(|line_id| {
3155                let line_object = self
3156                    .scene_graph
3157                    .objects
3158                    .get(line_id.0)
3159                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3160                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3161                    let kind = line_object.kind.human_friendly_kind_with_article();
3162                    return Err(KclError::refactor(format!(
3163                        "This constraint only works on Segments, but you selected {kind}"
3164                    )));
3165                };
3166                let Segment::Line(_) = line_segment else {
3167                    let kind = line_segment.human_friendly_kind_with_article();
3168                    return Err(KclError::refactor(format!(
3169                        "Only lines can be made equal length, but you selected {kind}"
3170                    )));
3171                };
3172
3173                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3174            })
3175            .collect::<Result<Vec<_>, _>>()?;
3176
3177        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3178            elements: line_asts,
3179            digest: None,
3180            non_code_meta: Default::default(),
3181        })));
3182
3183        self.mutate_ast(
3184            new_ast,
3185            constraint_id,
3186            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3187        )?;
3188        Ok(())
3189    }
3190
3191    /// Updates the parallel constraint with the given lines.
3192    fn edit_parallel_constraint(
3193        &mut self,
3194        new_ast: &mut ast::Node<ast::Program>,
3195        constraint_id: ObjectId,
3196        lines: Vec<ObjectId>,
3197    ) -> Result<(), KclError> {
3198        if lines.len() < 2 {
3199            return Err(KclError::refactor(format!(
3200                "Parallel constraint must have at least 2 lines, got {}",
3201                lines.len()
3202            )));
3203        }
3204
3205        let line_asts = lines
3206            .iter()
3207            .map(|line_id| {
3208                let line_object = self
3209                    .scene_graph
3210                    .objects
3211                    .get(line_id.0)
3212                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3213                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3214                    let kind = line_object.kind.human_friendly_kind_with_article();
3215                    return Err(KclError::refactor(format!(
3216                        "This constraint only works on Segments, but you selected {kind}"
3217                    )));
3218                };
3219                let Segment::Line(_) = line_segment else {
3220                    let kind = line_segment.human_friendly_kind_with_article();
3221                    return Err(KclError::refactor(format!(
3222                        "Only lines can be made parallel, but you selected {kind}"
3223                    )));
3224                };
3225
3226                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3227            })
3228            .collect::<Result<Vec<_>, _>>()?;
3229
3230        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3231            elements: line_asts,
3232            digest: None,
3233            non_code_meta: Default::default(),
3234        })));
3235
3236        self.mutate_ast(
3237            new_ast,
3238            constraint_id,
3239            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3240        )?;
3241        Ok(())
3242    }
3243
3244    /// Updates the equalRadius constraint with the given segments.
3245    fn edit_equal_radius_constraint(
3246        &mut self,
3247        new_ast: &mut ast::Node<ast::Program>,
3248        constraint_id: ObjectId,
3249        input: Vec<ObjectId>,
3250    ) -> Result<(), KclError> {
3251        if input.len() < 2 {
3252            return Err(KclError::refactor(format!(
3253                "equalRadius constraint must have at least 2 segments, got {}",
3254                input.len()
3255            )));
3256        }
3257
3258        let input_asts = input
3259            .iter()
3260            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3261            .collect::<Result<Vec<_>, _>>()?;
3262
3263        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3264            elements: input_asts,
3265            digest: None,
3266            non_code_meta: Default::default(),
3267        })));
3268
3269        self.mutate_ast(
3270            new_ast,
3271            constraint_id,
3272            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3273        )?;
3274        Ok(())
3275    }
3276
3277    async fn execute_after_edit(
3278        &mut self,
3279        ctx: &ExecutorContext,
3280        sketch: ObjectId,
3281        sketch_block_ref: AstNodeRef,
3282        new_ast: &mut ast::Node<ast::Program>,
3283        options: ExecuteAfterEditOptions,
3284    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3285        let ExecuteAfterEditOptions {
3286            segment_ids_edited,
3287            edit_kind,
3288            commit_solved_initial_guesses,
3289        } = options;
3290
3291        // Convert to string source to create real source ranges.
3292        let new_source = source_from_ast(new_ast);
3293        // Parse the new KCL source.
3294        let (new_program, errors) = Program::parse(&new_source)
3295            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3296        if !errors.is_empty() {
3297            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3298                "Error parsing KCL source after editing: {errors:?}"
3299            ))));
3300        }
3301        let Some(new_program) = new_program else {
3302            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3303                "No AST produced after editing".to_string(),
3304            )));
3305        };
3306
3307        // TODO: sketch-api: make sure to only set this if there are no errors.
3308        self.program = new_program.clone();
3309
3310        // Truncate after the sketch block for mock execution.
3311        let is_delete = edit_kind.is_delete();
3312        let truncated_program = {
3313            let mut truncated_program = new_program;
3314            only_sketch_block(
3315                &mut truncated_program.ast,
3316                &sketch_block_ref,
3317                edit_kind.to_change_kind(),
3318            )
3319            .map_err(KclErrorWithOutputs::no_outputs)?;
3320            truncated_program
3321        };
3322
3323        // Execute.
3324        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3325        let mock_config = MockConfig {
3326            sketch_block_id: Some(sketch),
3327            freedom_analysis: is_delete,
3328            segment_ids_edited: segment_ids_edited.clone(),
3329            drag_anchors,
3330            ..Default::default()
3331        };
3332        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3333
3334        // Uses freedom_analysis: is_delete
3335        let outcome = self.update_state_after_exec(outcome, is_delete);
3336
3337        let src_delta = if commit_solved_initial_guesses {
3338            self.commit_var_solutions_to_program(&outcome, "editing")?
3339        } else {
3340            SourceDelta { text: new_source }
3341        };
3342        let scene_graph_delta = SceneGraphDelta {
3343            new_graph: self.scene_graph_for_ui(),
3344            invalidates_ids: is_delete,
3345            new_objects: Vec::new(),
3346            exec_outcome: outcome,
3347        };
3348        Ok((src_delta, scene_graph_delta))
3349    }
3350
3351    async fn execute_after_delete_sketch(
3352        &mut self,
3353        ctx: &ExecutorContext,
3354        new_ast: &mut ast::Node<ast::Program>,
3355    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3356        // Convert to string source to create real source ranges.
3357        let new_source = source_from_ast(new_ast);
3358        // Parse the new KCL source.
3359        let (new_program, errors) = Program::parse(&new_source)
3360            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3361        if !errors.is_empty() {
3362            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3363                "Error parsing KCL source after editing: {errors:?}"
3364            ))));
3365        }
3366        let Some(new_program) = new_program else {
3367            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3368                "No AST produced after editing".to_string(),
3369            )));
3370        };
3371
3372        // Make sure to only set this if there are no errors.
3373        self.program = new_program.clone();
3374
3375        // We deleted the entire sketch block. It doesn't make sense to truncate
3376        // and execute only the sketch block. We execute the whole program with
3377        // a real engine.
3378
3379        // Execute.
3380        let outcome = ctx.run_with_caching(new_program).await?;
3381        let freedom_analysis_ran = true;
3382
3383        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3384
3385        let src_delta = SourceDelta { text: new_source };
3386        let scene_graph_delta = SceneGraphDelta {
3387            new_graph: self.scene_graph_for_ui(),
3388            invalidates_ids: true,
3389            new_objects: Vec::new(),
3390            exec_outcome: outcome,
3391        };
3392        Ok((src_delta, scene_graph_delta))
3393    }
3394
3395    /// Map a point object id into an AST reference expression for use in
3396    /// constraints. If the point is owned by a segment (line or arc), we
3397    /// reference the appropriate property on that segment (e.g. `line1.start`,
3398    /// `arc1.center`). Otherwise we reference the point directly.
3399    fn point_id_to_ast_reference(
3400        &self,
3401        point_id: ObjectId,
3402        new_ast: &mut ast::Node<ast::Program>,
3403    ) -> Result<ast::Expr, KclError> {
3404        let point_object = self
3405            .scene_graph
3406            .objects
3407            .get(point_id.0)
3408            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3409        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3410            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3411        };
3412        let Segment::Point(point) = point_segment else {
3413            return Err(KclError::refactor(format!(
3414                "Only points are currently supported: {point_object:?}"
3415            )));
3416        };
3417
3418        if let Some(owner_id) = point.owner {
3419            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3420                KclError::refactor(format!(
3421                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3422                ))
3423            })?;
3424            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3425                return Err(KclError::refactor(format!(
3426                    "Owner of point is not a segment, but found {}",
3427                    owner_object.kind.human_friendly_kind_with_article()
3428                )));
3429            };
3430
3431            match owner_segment {
3432                Segment::Line(line) => {
3433                    let property = if line.start == point_id {
3434                        LINE_PROPERTY_START
3435                    } else if line.end == point_id {
3436                        LINE_PROPERTY_END
3437                    } else {
3438                        return Err(KclError::refactor(format!(
3439                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3440                        )));
3441                    };
3442                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3443                }
3444                Segment::Arc(arc) => {
3445                    let property = if arc.start == point_id {
3446                        ARC_PROPERTY_START
3447                    } else if arc.end == point_id {
3448                        ARC_PROPERTY_END
3449                    } else if arc.center == point_id {
3450                        ARC_PROPERTY_CENTER
3451                    } else {
3452                        return Err(KclError::refactor(format!(
3453                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3454                        )));
3455                    };
3456                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3457                }
3458                Segment::Circle(circle) => {
3459                    let property = if circle.start == point_id {
3460                        CIRCLE_PROPERTY_START
3461                    } else if circle.center == point_id {
3462                        CIRCLE_PROPERTY_CENTER
3463                    } else {
3464                        return Err(KclError::refactor(format!(
3465                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3466                        )));
3467                    };
3468                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3469                }
3470                Segment::ControlPointSpline(spline) => {
3471                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3472                        return Err(KclError::refactor(format!(
3473                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3474                        )));
3475                    };
3476                    let owner_expr =
3477                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3478                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3479                    Ok(create_index_expression(controls_expr, index))
3480                }
3481                _ => Err(KclError::refactor(format!(
3482                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3483                ))),
3484            }
3485        } else {
3486            // Standalone point.
3487            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3488        }
3489    }
3490
3491    fn line_id_to_ast_reference(
3492        &self,
3493        line_id: ObjectId,
3494        new_ast: &mut ast::Node<ast::Program>,
3495    ) -> Result<ast::Expr, KclError> {
3496        let line_object = self
3497            .scene_graph
3498            .objects
3499            .get(line_id.0)
3500            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3501        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3502            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3503        };
3504        let Segment::Line(line) = line_segment else {
3505            return Err(KclError::refactor(format!(
3506                "Only lines are currently supported: {line_object:?}"
3507            )));
3508        };
3509
3510        if let Some(owner_id) = line.owner {
3511            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3512                KclError::refactor(format!(
3513                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3514                ))
3515            })?;
3516            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3517                return Err(KclError::refactor(format!(
3518                    "Owner of line is not a segment, but found {}",
3519                    owner_object.kind.human_friendly_kind_with_article()
3520                )));
3521            };
3522
3523            match owner_segment {
3524                Segment::ControlPointSpline(spline) => {
3525                    let Some(index) = spline
3526                        .controls
3527                        .windows(2)
3528                        .position(|window| window[0] == line.start && window[1] == line.end)
3529                    else {
3530                        return Err(KclError::refactor(format!(
3531                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3532                        )));
3533                    };
3534                    let owner_expr =
3535                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3536                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3537                    Ok(create_index_expression(edges_expr, index))
3538                }
3539                _ => Err(KclError::refactor(format!(
3540                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3541                ))),
3542            }
3543        } else {
3544            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3545        }
3546    }
3547
3548    fn coincident_segment_to_ast(
3549        &self,
3550        segment: &ConstraintSegment,
3551        new_ast: &mut ast::Node<ast::Program>,
3552    ) -> Result<ast::Expr, KclError> {
3553        match segment {
3554            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3555            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3556        }
3557    }
3558
3559    fn segment_id_to_constraint_ast_reference(
3560        &self,
3561        segment_id: ObjectId,
3562        new_ast: &mut ast::Node<ast::Program>,
3563    ) -> Result<ast::Expr, KclError> {
3564        let segment_object = self
3565            .scene_graph
3566            .objects
3567            .get(segment_id.0)
3568            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3569        let ObjectKind::Segment { segment } = &segment_object.kind else {
3570            return Err(KclError::refactor(format!(
3571                "Object is not a segment, it is {}",
3572                segment_object.kind.human_friendly_kind_with_article()
3573            )));
3574        };
3575
3576        match segment {
3577            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3578            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3579            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3580            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3581            Segment::ControlPointSpline(_) => {
3582                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3583            }
3584        }
3585    }
3586
3587    fn axis_constraint_segment_to_ast(
3588        &self,
3589        segment: &ConstraintSegment,
3590        new_ast: &mut ast::Node<ast::Program>,
3591    ) -> Result<ast::Expr, KclError> {
3592        match segment {
3593            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3594            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3595        }
3596    }
3597
3598    async fn add_coincident(
3599        &mut self,
3600        sketch: ObjectId,
3601        coincident: Coincident,
3602        new_ast: &mut ast::Node<ast::Program>,
3603    ) -> Result<AstNodeRef, KclError> {
3604        let sketch_id = sketch;
3605        for segment in &coincident.segments {
3606            let ConstraintSegment::Segment(segment_id) = segment else {
3607                continue;
3608            };
3609            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3610                continue;
3611            };
3612            if matches!(
3613                segment_object.kind,
3614                ObjectKind::Segment {
3615                    segment: Segment::ControlPointSpline(_)
3616                }
3617            ) {
3618                return Err(KclError::refactor(
3619                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3620                        .to_owned(),
3621                ));
3622            }
3623        }
3624        let segment_asts = coincident
3625            .segments
3626            .iter()
3627            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3628            .collect::<Result<Vec<_>, _>>()?;
3629        if segment_asts.len() < 2 {
3630            return Err(KclError::refactor(format!(
3631                "Coincident constraint must have at least 2 inputs, got {}",
3632                segment_asts.len()
3633            )));
3634        }
3635
3636        // Create the coincident() call using shared helper.
3637        let coincident_ast = create_coincident_ast(segment_asts);
3638
3639        // Add the line to the AST of the sketch block.
3640        let (sketch_block_ref, _) = self.mutate_ast(
3641            new_ast,
3642            sketch_id,
3643            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3644        )?;
3645        Ok(sketch_block_ref)
3646    }
3647
3648    async fn add_distance(
3649        &mut self,
3650        sketch: ObjectId,
3651        distance: Distance,
3652        new_ast: &mut ast::Node<ast::Program>,
3653    ) -> Result<AstNodeRef, KclError> {
3654        let sketch_id = sketch;
3655        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3656            [pt0, pt1] => [
3657                self.coincident_segment_to_ast(pt0, new_ast)?,
3658                self.coincident_segment_to_ast(pt1, new_ast)?,
3659            ],
3660            _ => {
3661                return Err(KclError::refactor(format!(
3662                    "Distance constraint must have exactly 2 points, got {}",
3663                    distance.points.len()
3664                )));
3665            }
3666        };
3667
3668        let arguments = match &distance.label_position {
3669            Some(label_position) => vec![ast::LabeledArg {
3670                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3671                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3672            }],
3673            None => Default::default(),
3674        };
3675
3676        // Create the distance() call.
3677        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3678            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3679            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3680                ast::ArrayExpression {
3681                    elements: vec![pt0_ast, pt1_ast],
3682                    digest: None,
3683                    non_code_meta: Default::default(),
3684                },
3685            )))),
3686            arguments,
3687            digest: None,
3688            non_code_meta: Default::default(),
3689        })));
3690        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3691            left: distance_call_ast,
3692            operator: ast::BinaryOperator::Eq,
3693            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3694                value: ast::LiteralValue::Number {
3695                    value: distance.distance.value,
3696                    suffix: distance.distance.units,
3697                },
3698                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3699                    KclError::refactor(format!(
3700                        "Could not format numeric suffix: {:?}",
3701                        distance.distance.units
3702                    ))
3703                })?,
3704                digest: None,
3705            }))),
3706            digest: None,
3707        })));
3708
3709        // Add the line to the AST of the sketch block.
3710        let (sketch_block_ref, _) = self.mutate_ast(
3711            new_ast,
3712            sketch_id,
3713            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3714        )?;
3715        Ok(sketch_block_ref)
3716    }
3717
3718    async fn add_angle(
3719        &mut self,
3720        sketch: ObjectId,
3721        angle: Angle,
3722        new_ast: &mut ast::Node<ast::Program>,
3723    ) -> Result<AstNodeRef, KclError> {
3724        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3725            return Err(KclError::refactor(format!(
3726                "Angle constraint must have exactly 2 lines, got {}",
3727                angle.lines.len()
3728            )));
3729        };
3730        let sketch_id = sketch;
3731
3732        // Map the runtime objects back to variable names.
3733        let line0_object = self
3734            .scene_graph
3735            .objects
3736            .get(l0_id.0)
3737            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3738        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3739            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3740        };
3741        let Segment::Line(_) = line0_segment else {
3742            return Err(KclError::refactor(format!(
3743                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3744            )));
3745        };
3746        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3747
3748        let line1_object = self
3749            .scene_graph
3750            .objects
3751            .get(l1_id.0)
3752            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3753        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3754            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3755        };
3756        let Segment::Line(_) = line1_segment else {
3757            return Err(KclError::refactor(format!(
3758                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3759            )));
3760        };
3761        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3762
3763        // Create the angle() call.
3764        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3765            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3766            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3767                ast::ArrayExpression {
3768                    elements: vec![l0_ast, l1_ast],
3769                    digest: None,
3770                    non_code_meta: Default::default(),
3771                },
3772            )))),
3773            arguments: Default::default(),
3774            digest: None,
3775            non_code_meta: Default::default(),
3776        })));
3777        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3778            left: angle_call_ast,
3779            operator: ast::BinaryOperator::Eq,
3780            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3781                value: ast::LiteralValue::Number {
3782                    value: angle.angle.value,
3783                    suffix: angle.angle.units,
3784                },
3785                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3786                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3787                })?,
3788                digest: None,
3789            }))),
3790            digest: None,
3791        })));
3792
3793        // Add the line to the AST of the sketch block.
3794        let (sketch_block_ref, _) = self.mutate_ast(
3795            new_ast,
3796            sketch_id,
3797            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3798        )?;
3799        Ok(sketch_block_ref)
3800    }
3801
3802    async fn add_tangent(
3803        &mut self,
3804        sketch: ObjectId,
3805        tangent: Tangent,
3806        new_ast: &mut ast::Node<ast::Program>,
3807    ) -> Result<AstNodeRef, KclError> {
3808        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3809            return Err(KclError::refactor(format!(
3810                "Tangent constraint must have exactly 2 segments, got {}",
3811                tangent.input.len()
3812            )));
3813        };
3814        let sketch_id = sketch;
3815
3816        let seg0_object = self
3817            .scene_graph
3818            .objects
3819            .get(seg0_id.0)
3820            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3821        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3822            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3823        };
3824        let seg0_ast = match seg0_segment {
3825            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3826                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3827            }
3828            _ => {
3829                return Err(KclError::refactor(format!(
3830                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3831                )));
3832            }
3833        };
3834
3835        let seg1_object = self
3836            .scene_graph
3837            .objects
3838            .get(seg1_id.0)
3839            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3840        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3841            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3842        };
3843        let seg1_ast = match seg1_segment {
3844            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3845                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3846            }
3847            _ => {
3848                return Err(KclError::refactor(format!(
3849                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3850                )));
3851            }
3852        };
3853
3854        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3855        let (sketch_block_ref, _) = self.mutate_ast(
3856            new_ast,
3857            sketch_id,
3858            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3859        )?;
3860        Ok(sketch_block_ref)
3861    }
3862
3863    async fn add_symmetric(
3864        &mut self,
3865        sketch: ObjectId,
3866        symmetric: Symmetric,
3867        new_ast: &mut ast::Node<ast::Program>,
3868    ) -> Result<AstNodeRef, KclError> {
3869        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3870            return Err(KclError::refactor(format!(
3871                "Symmetric constraint must have exactly 2 inputs, got {}",
3872                symmetric.input.len()
3873            )));
3874        };
3875        let sketch_id = sketch;
3876
3877        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3878        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3879        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3880
3881        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3882        let (sketch_block_ref, _) = self.mutate_ast(
3883            new_ast,
3884            sketch_id,
3885            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3886        )?;
3887        Ok(sketch_block_ref)
3888    }
3889
3890    async fn add_midpoint(
3891        &mut self,
3892        sketch: ObjectId,
3893        midpoint: Midpoint,
3894        new_ast: &mut ast::Node<ast::Program>,
3895    ) -> Result<AstNodeRef, KclError> {
3896        let sketch_id = sketch;
3897        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3898
3899        let segment_object = self
3900            .scene_graph
3901            .objects
3902            .get(midpoint.segment.0)
3903            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3904        let ObjectKind::Segment {
3905            segment: midpoint_segment,
3906        } = &segment_object.kind
3907        else {
3908            return Err(KclError::refactor(format!(
3909                "Object must be a segment, but it was {}",
3910                segment_object.kind.human_friendly_kind_with_article()
3911            )));
3912        };
3913        let segment_ast = match midpoint_segment {
3914            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3915            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3916            _ => {
3917                return Err(KclError::refactor(format!(
3918                    "Midpoint target must be a line or arc segment but it was {}",
3919                    midpoint_segment.human_friendly_kind_with_article()
3920                )));
3921            }
3922        };
3923
3924        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3925        let (sketch_block_ref, _) = self.mutate_ast(
3926            new_ast,
3927            sketch_id,
3928            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3929        )?;
3930        Ok(sketch_block_ref)
3931    }
3932
3933    async fn add_equal_radius(
3934        &mut self,
3935        sketch: ObjectId,
3936        equal_radius: EqualRadius,
3937        new_ast: &mut ast::Node<ast::Program>,
3938    ) -> Result<AstNodeRef, KclError> {
3939        if equal_radius.input.len() < 2 {
3940            return Err(KclError::refactor(format!(
3941                "equalRadius constraint must have at least 2 segments, got {}",
3942                equal_radius.input.len()
3943            )));
3944        }
3945
3946        let sketch_id = sketch;
3947        let input_asts = equal_radius
3948            .input
3949            .iter()
3950            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3951            .collect::<Result<Vec<_>, _>>()?;
3952
3953        let equal_radius_ast = create_equal_radius_ast(input_asts);
3954        let (sketch_block_ref, _) = self.mutate_ast(
3955            new_ast,
3956            sketch_id,
3957            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3958        )?;
3959        Ok(sketch_block_ref)
3960    }
3961
3962    async fn add_radius(
3963        &mut self,
3964        sketch: ObjectId,
3965        radius: Radius,
3966        new_ast: &mut ast::Node<ast::Program>,
3967    ) -> Result<AstNodeRef, KclError> {
3968        let params = ArcSizeConstraintParams {
3969            points: vec![radius.arc],
3970            function_name: RADIUS_FN,
3971            value: radius.radius.value,
3972            units: radius.radius.units,
3973            label_position: radius.label_position,
3974            constraint_type_name: "Radius",
3975        };
3976        self.add_arc_size_constraint(sketch, params, new_ast).await
3977    }
3978
3979    async fn add_diameter(
3980        &mut self,
3981        sketch: ObjectId,
3982        diameter: Diameter,
3983        new_ast: &mut ast::Node<ast::Program>,
3984    ) -> Result<AstNodeRef, KclError> {
3985        let params = ArcSizeConstraintParams {
3986            points: vec![diameter.arc],
3987            function_name: DIAMETER_FN,
3988            value: diameter.diameter.value,
3989            units: diameter.diameter.units,
3990            label_position: diameter.label_position,
3991            constraint_type_name: "Diameter",
3992        };
3993        self.add_arc_size_constraint(sketch, params, new_ast).await
3994    }
3995
3996    async fn add_fixed_constraints(
3997        &mut self,
3998        sketch: ObjectId,
3999        points: Vec<FixedPoint>,
4000        new_ast: &mut ast::Node<ast::Program>,
4001    ) -> Result<AstNodeRef, KclError> {
4002        let mut sketch_block_ref = None;
4003
4004        for fixed_point in points {
4005            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4006            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4007                .map_err(|err| KclError::refactor(err.to_string()))?;
4008
4009            let (sketch_ref, _) = self.mutate_ast(
4010                new_ast,
4011                sketch,
4012                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4013            )?;
4014            sketch_block_ref = Some(sketch_ref);
4015        }
4016
4017        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4018    }
4019
4020    async fn add_arc_size_constraint(
4021        &mut self,
4022        sketch: ObjectId,
4023        params: ArcSizeConstraintParams,
4024        new_ast: &mut ast::Node<ast::Program>,
4025    ) -> Result<AstNodeRef, KclError> {
4026        let sketch_id = sketch;
4027
4028        // Constraint must have exactly 1 argument (arc segment)
4029        if params.points.len() != 1 {
4030            return Err(KclError::refactor(format!(
4031                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4032                params.constraint_type_name,
4033                params.points.len()
4034            )));
4035        }
4036
4037        let arc_id = params.points[0];
4038        let arc_object = self
4039            .scene_graph
4040            .objects
4041            .get(arc_id.0)
4042            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4043        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4044            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4045        };
4046        let ref_type = match arc_segment {
4047            Segment::Arc(_) => ARC_VARIABLE,
4048            Segment::Circle(_) => CIRCLE_VARIABLE,
4049            _ => {
4050                return Err(KclError::refactor(format!(
4051                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4052                    params.constraint_type_name
4053                )));
4054            }
4055        };
4056        // Reference the arc/circle segment directly
4057        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4058        let arguments = match &params.label_position {
4059            Some(label_position) => vec![ast::LabeledArg {
4060                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4061                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4062            }],
4063            None => Default::default(),
4064        };
4065
4066        // Create the function call.
4067        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4068            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4069            unlabeled: Some(arc_ast),
4070            arguments,
4071            digest: None,
4072            non_code_meta: Default::default(),
4073        })));
4074        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4075            left: call_ast,
4076            operator: ast::BinaryOperator::Eq,
4077            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4078                value: ast::LiteralValue::Number {
4079                    value: params.value,
4080                    suffix: params.units,
4081                },
4082                raw: format_number_literal(params.value, params.units, None)
4083                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4084                digest: None,
4085            }))),
4086            digest: None,
4087        })));
4088
4089        // Add the line to the AST of the sketch block.
4090        let (sketch_block_ref, _) = self.mutate_ast(
4091            new_ast,
4092            sketch_id,
4093            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4094        )?;
4095        Ok(sketch_block_ref)
4096    }
4097
4098    async fn add_horizontal_distance(
4099        &mut self,
4100        sketch: ObjectId,
4101        distance: Distance,
4102        new_ast: &mut ast::Node<ast::Program>,
4103    ) -> Result<AstNodeRef, KclError> {
4104        let sketch_id = sketch;
4105        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4106            [pt0, pt1] => [
4107                self.coincident_segment_to_ast(pt0, new_ast)?,
4108                self.coincident_segment_to_ast(pt1, new_ast)?,
4109            ],
4110            _ => {
4111                return Err(KclError::refactor(format!(
4112                    "Horizontal distance constraint must have exactly 2 points, got {}",
4113                    distance.points.len()
4114                )));
4115            }
4116        };
4117
4118        let arguments = match &distance.label_position {
4119            Some(label_position) => vec![ast::LabeledArg {
4120                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4121                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4122            }],
4123            None => Default::default(),
4124        };
4125
4126        // Create the horizontalDistance() call.
4127        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4128            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4129            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4130                ast::ArrayExpression {
4131                    elements: vec![pt0_ast, pt1_ast],
4132                    digest: None,
4133                    non_code_meta: Default::default(),
4134                },
4135            )))),
4136            arguments,
4137            digest: None,
4138            non_code_meta: Default::default(),
4139        })));
4140        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4141            left: distance_call_ast,
4142            operator: ast::BinaryOperator::Eq,
4143            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4144                value: ast::LiteralValue::Number {
4145                    value: distance.distance.value,
4146                    suffix: distance.distance.units,
4147                },
4148                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4149                    KclError::refactor(format!(
4150                        "Could not format numeric suffix: {:?}",
4151                        distance.distance.units
4152                    ))
4153                })?,
4154                digest: None,
4155            }))),
4156            digest: None,
4157        })));
4158
4159        // Add the line to the AST of the sketch block.
4160        let (sketch_block_ref, _) = self.mutate_ast(
4161            new_ast,
4162            sketch_id,
4163            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4164        )?;
4165        Ok(sketch_block_ref)
4166    }
4167
4168    async fn add_vertical_distance(
4169        &mut self,
4170        sketch: ObjectId,
4171        distance: Distance,
4172        new_ast: &mut ast::Node<ast::Program>,
4173    ) -> Result<AstNodeRef, KclError> {
4174        let sketch_id = sketch;
4175        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4176            [pt0, pt1] => [
4177                self.coincident_segment_to_ast(pt0, new_ast)?,
4178                self.coincident_segment_to_ast(pt1, new_ast)?,
4179            ],
4180            _ => {
4181                return Err(KclError::refactor(format!(
4182                    "Vertical distance constraint must have exactly 2 points, got {}",
4183                    distance.points.len()
4184                )));
4185            }
4186        };
4187
4188        let arguments = match &distance.label_position {
4189            Some(label_position) => vec![ast::LabeledArg {
4190                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4191                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4192            }],
4193            None => Default::default(),
4194        };
4195
4196        // Create the verticalDistance() call.
4197        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4198            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4199            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4200                ast::ArrayExpression {
4201                    elements: vec![pt0_ast, pt1_ast],
4202                    digest: None,
4203                    non_code_meta: Default::default(),
4204                },
4205            )))),
4206            arguments,
4207            digest: None,
4208            non_code_meta: Default::default(),
4209        })));
4210        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4211            left: distance_call_ast,
4212            operator: ast::BinaryOperator::Eq,
4213            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4214                value: ast::LiteralValue::Number {
4215                    value: distance.distance.value,
4216                    suffix: distance.distance.units,
4217                },
4218                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4219                    KclError::refactor(format!(
4220                        "Could not format numeric suffix: {:?}",
4221                        distance.distance.units
4222                    ))
4223                })?,
4224                digest: None,
4225            }))),
4226            digest: None,
4227        })));
4228
4229        // Add the line to the AST of the sketch block.
4230        let (sketch_block_ref, _) = self.mutate_ast(
4231            new_ast,
4232            sketch_id,
4233            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4234        )?;
4235        Ok(sketch_block_ref)
4236    }
4237
4238    async fn add_horizontal(
4239        &mut self,
4240        sketch: ObjectId,
4241        horizontal: Horizontal,
4242        new_ast: &mut ast::Node<ast::Program>,
4243    ) -> Result<AstNodeRef, KclError> {
4244        let sketch_id = sketch;
4245
4246        // Map the runtime objects back to variable names.
4247        let first_arg_ast = match horizontal {
4248            Horizontal::Line { line } => {
4249                let line_object = self
4250                    .scene_graph
4251                    .objects
4252                    .get(line.0)
4253                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4254                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4255                    let kind = line_object.kind.human_friendly_kind_with_article();
4256                    return Err(KclError::refactor(format!(
4257                        "This constraint only works on Segments, but you selected {kind}"
4258                    )));
4259                };
4260                let Segment::Line(_) = line_segment else {
4261                    return Err(KclError::refactor(format!(
4262                        "Only lines can be made horizontal, but you selected {}",
4263                        line_segment.human_friendly_kind_with_article(),
4264                    )));
4265                };
4266                self.line_id_to_ast_reference(line, new_ast)?
4267            }
4268            Horizontal::Points { points } => {
4269                let point_asts = points
4270                    .iter()
4271                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4272                    .collect::<Result<Vec<_>, _>>()?;
4273                ast::ArrayExpression::new(point_asts).into()
4274            }
4275        };
4276        // Create the horizontal() call using shared helper.
4277        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4278
4279        // Add the line to the AST of the sketch block.
4280        let (sketch_block_ref, _) = self.mutate_ast(
4281            new_ast,
4282            sketch_id,
4283            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4284        )?;
4285        Ok(sketch_block_ref)
4286    }
4287
4288    async fn add_lines_equal_length(
4289        &mut self,
4290        sketch: ObjectId,
4291        lines_equal_length: LinesEqualLength,
4292        new_ast: &mut ast::Node<ast::Program>,
4293    ) -> Result<AstNodeRef, KclError> {
4294        if lines_equal_length.lines.len() < 2 {
4295            return Err(KclError::refactor(format!(
4296                "Lines equal length constraint must have at least 2 lines, got {}",
4297                lines_equal_length.lines.len()
4298            )));
4299        };
4300
4301        let sketch_id = sketch;
4302
4303        // Map the runtime objects back to variable names.
4304        let line_asts = lines_equal_length
4305            .lines
4306            .iter()
4307            .map(|line_id| {
4308                let line_object = self
4309                    .scene_graph
4310                    .objects
4311                    .get(line_id.0)
4312                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4313                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4314                    let kind = line_object.kind.human_friendly_kind_with_article();
4315                    return Err(KclError::refactor(format!(
4316                        "This constraint only works on Segments, but you selected {kind}"
4317                    )));
4318                };
4319                let Segment::Line(_) = line_segment else {
4320                    let kind = line_segment.human_friendly_kind_with_article();
4321                    return Err(KclError::refactor(format!(
4322                        "Only lines can be made equal length, but you selected {kind}"
4323                    )));
4324                };
4325
4326                self.line_id_to_ast_reference(*line_id, new_ast)
4327            })
4328            .collect::<Result<Vec<_>, _>>()?;
4329
4330        // Create the equalLength() call using shared helper.
4331        let equal_length_ast = create_equal_length_ast(line_asts);
4332
4333        // Add the constraint to the AST of the sketch block.
4334        let (sketch_block_ref, _) = self.mutate_ast(
4335            new_ast,
4336            sketch_id,
4337            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4338        )?;
4339        Ok(sketch_block_ref)
4340    }
4341
4342    fn equal_radius_segment_id_to_ast_reference(
4343        &mut self,
4344        segment_id: ObjectId,
4345        new_ast: &mut ast::Node<ast::Program>,
4346    ) -> Result<ast::Expr, KclError> {
4347        let segment_object = self
4348            .scene_graph
4349            .objects
4350            .get(segment_id.0)
4351            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4352        let ObjectKind::Segment { segment } = &segment_object.kind else {
4353            return Err(KclError::refactor(format!(
4354                "Object is not a segment, it was {}",
4355                segment_object.kind.human_friendly_kind_with_article()
4356            )));
4357        };
4358
4359        let ref_type = match segment {
4360            Segment::Arc(_) => ARC_VARIABLE,
4361            Segment::Circle(_) => CIRCLE_VARIABLE,
4362            _ => {
4363                return Err(KclError::refactor(format!(
4364                    "equalRadius supports only arc/circle segments, got {}",
4365                    segment.human_friendly_kind_with_article()
4366                )));
4367            }
4368        };
4369
4370        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4371    }
4372
4373    fn symmetric_input_id_to_ast_reference(
4374        &mut self,
4375        segment_id: ObjectId,
4376        new_ast: &mut ast::Node<ast::Program>,
4377    ) -> Result<ast::Expr, KclError> {
4378        let segment_object = self
4379            .scene_graph
4380            .objects
4381            .get(segment_id.0)
4382            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4383        let ObjectKind::Segment { segment } = &segment_object.kind else {
4384            return Err(KclError::refactor(format!(
4385                "Object is not a segment, it was {}",
4386                segment_object.kind.human_friendly_kind_with_article()
4387            )));
4388        };
4389
4390        match segment {
4391            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4392            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4393            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4394            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4395            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4396                "Symmetric does not yet support control point splines".to_owned(),
4397            )),
4398        }
4399    }
4400
4401    fn symmetric_axis_id_to_ast_reference(
4402        &mut self,
4403        segment_id: ObjectId,
4404        new_ast: &mut ast::Node<ast::Program>,
4405    ) -> Result<ast::Expr, KclError> {
4406        let segment_object = self
4407            .scene_graph
4408            .objects
4409            .get(segment_id.0)
4410            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4411        let ObjectKind::Segment { segment } = &segment_object.kind else {
4412            return Err(KclError::refactor(format!(
4413                "Object is not a segment, it was {}",
4414                segment_object.kind.human_friendly_kind_with_article()
4415            )));
4416        };
4417        match segment {
4418            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4419            _ => Err(KclError::refactor(format!(
4420                "Symmetric axis must be a line, got {}",
4421                segment.human_friendly_kind_with_article()
4422            ))),
4423        }
4424    }
4425
4426    async fn add_parallel(
4427        &mut self,
4428        sketch: ObjectId,
4429        parallel: Parallel,
4430        new_ast: &mut ast::Node<ast::Program>,
4431    ) -> Result<AstNodeRef, KclError> {
4432        if parallel.lines.len() < 2 {
4433            return Err(KclError::refactor(format!(
4434                "Parallel constraint must have at least 2 lines, got {}",
4435                parallel.lines.len()
4436            )));
4437        };
4438
4439        let sketch_id = sketch;
4440
4441        let line_asts = parallel
4442            .lines
4443            .iter()
4444            .map(|line_id| {
4445                let line_object = self
4446                    .scene_graph
4447                    .objects
4448                    .get(line_id.0)
4449                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4450                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4451                    let kind = line_object.kind.human_friendly_kind_with_article();
4452                    return Err(KclError::refactor(format!(
4453                        "This constraint only works on Segments, but you selected {kind}"
4454                    )));
4455                };
4456                let Segment::Line(_) = line_segment else {
4457                    let kind = line_segment.human_friendly_kind_with_article();
4458                    return Err(KclError::refactor(format!(
4459                        "Only lines can be made parallel, but you selected {kind}"
4460                    )));
4461                };
4462
4463                self.line_id_to_ast_reference(*line_id, new_ast)
4464            })
4465            .collect::<Result<Vec<_>, _>>()?;
4466
4467        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4468            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4469            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4470                ast::ArrayExpression {
4471                    elements: line_asts,
4472                    digest: None,
4473                    non_code_meta: Default::default(),
4474                },
4475            )))),
4476            arguments: Default::default(),
4477            digest: None,
4478            non_code_meta: Default::default(),
4479        })));
4480
4481        let (sketch_block_ref, _) = self.mutate_ast(
4482            new_ast,
4483            sketch_id,
4484            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4485        )?;
4486        Ok(sketch_block_ref)
4487    }
4488
4489    async fn add_perpendicular(
4490        &mut self,
4491        sketch: ObjectId,
4492        perpendicular: Perpendicular,
4493        new_ast: &mut ast::Node<ast::Program>,
4494    ) -> Result<AstNodeRef, KclError> {
4495        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4496            .await
4497    }
4498
4499    async fn add_lines_at_angle_constraint(
4500        &mut self,
4501        sketch: ObjectId,
4502        angle_kind: LinesAtAngleKind,
4503        lines: Vec<ObjectId>,
4504        new_ast: &mut ast::Node<ast::Program>,
4505    ) -> Result<AstNodeRef, KclError> {
4506        let &[line0_id, line1_id] = lines.as_slice() else {
4507            return Err(KclError::refactor(format!(
4508                "{} constraint must have exactly 2 lines, got {}",
4509                angle_kind.to_function_name(),
4510                lines.len()
4511            )));
4512        };
4513
4514        let sketch_id = sketch;
4515
4516        // Map the runtime objects back to variable names.
4517        let line0_object = self
4518            .scene_graph
4519            .objects
4520            .get(line0_id.0)
4521            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4522        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4523            let kind = line0_object.kind.human_friendly_kind_with_article();
4524            return Err(KclError::refactor(format!(
4525                "This constraint only works on Segments, but you selected {kind}"
4526            )));
4527        };
4528        let Segment::Line(_) = line0_segment else {
4529            return Err(KclError::refactor(format!(
4530                "Only lines can be made {}, but you selected {}",
4531                angle_kind.to_function_name(),
4532                line0_segment.human_friendly_kind_with_article(),
4533            )));
4534        };
4535        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4536
4537        let line1_object = self
4538            .scene_graph
4539            .objects
4540            .get(line1_id.0)
4541            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4542        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4543            let kind = line1_object.kind.human_friendly_kind_with_article();
4544            return Err(KclError::refactor(format!(
4545                "This constraint only works on Segments, but you selected {kind}"
4546            )));
4547        };
4548        let Segment::Line(_) = line1_segment else {
4549            return Err(KclError::refactor(format!(
4550                "Only lines can be made {}, but you selected {}",
4551                angle_kind.to_function_name(),
4552                line1_segment.human_friendly_kind_with_article(),
4553            )));
4554        };
4555        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4556
4557        // Create the parallel() or perpendicular() call.
4558        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4559            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4560            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4561                ast::ArrayExpression {
4562                    elements: vec![line0_ast, line1_ast],
4563                    digest: None,
4564                    non_code_meta: Default::default(),
4565                },
4566            )))),
4567            arguments: Default::default(),
4568            digest: None,
4569            non_code_meta: Default::default(),
4570        })));
4571
4572        // Add the constraint to the AST of the sketch block.
4573        let (sketch_block_ref, _) = self.mutate_ast(
4574            new_ast,
4575            sketch_id,
4576            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4577        )?;
4578        Ok(sketch_block_ref)
4579    }
4580
4581    async fn add_vertical(
4582        &mut self,
4583        sketch: ObjectId,
4584        vertical: Vertical,
4585        new_ast: &mut ast::Node<ast::Program>,
4586    ) -> Result<AstNodeRef, KclError> {
4587        let sketch_id = sketch;
4588
4589        let first_arg_ast = match vertical {
4590            Vertical::Line { line } => {
4591                // Map the runtime objects back to variable names.
4592                let line_object = self
4593                    .scene_graph
4594                    .objects
4595                    .get(line.0)
4596                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4597                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4598                    let kind = line_object.kind.human_friendly_kind_with_article();
4599                    return Err(KclError::refactor(format!(
4600                        "This constraint only works on Segments, but you selected {kind}"
4601                    )));
4602                };
4603                let Segment::Line(_) = line_segment else {
4604                    return Err(KclError::refactor(format!(
4605                        "Only lines can be made vertical, but you selected {}",
4606                        line_segment.human_friendly_kind_with_article()
4607                    )));
4608                };
4609                self.line_id_to_ast_reference(line, new_ast)?
4610            }
4611            Vertical::Points { points } => {
4612                let point_asts = points
4613                    .iter()
4614                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4615                    .collect::<Result<Vec<_>, _>>()?;
4616                ast::ArrayExpression::new(point_asts).into()
4617            }
4618        };
4619        // Create the vertical() call using shared helper.
4620        let vertical_ast = create_vertical_ast(first_arg_ast);
4621
4622        // Add the line to the AST of the sketch block.
4623        let (sketch_block_ref, _) = self.mutate_ast(
4624            new_ast,
4625            sketch_id,
4626            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4627        )?;
4628        Ok(sketch_block_ref)
4629    }
4630
4631    async fn execute_after_add_constraint(
4632        &mut self,
4633        ctx: &ExecutorContext,
4634        sketch_id: ObjectId,
4635        sketch_block_ref: AstNodeRef,
4636        new_ast: &mut ast::Node<ast::Program>,
4637    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4638        // Convert to string source to create real source ranges.
4639        let new_source = source_from_ast(new_ast);
4640        // Parse the new KCL source.
4641        let (new_program, errors) = Program::parse(&new_source)
4642            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4643        if !errors.is_empty() {
4644            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4645                "Error parsing KCL source after adding constraint: {errors:?}"
4646            ))));
4647        }
4648        let Some(new_program) = new_program else {
4649            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4650                "No AST produced after adding constraint".to_string(),
4651            )));
4652        };
4653        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4654            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4655                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4656            )))
4657        })?;
4658
4659        // Truncate after the sketch block for mock execution.
4660        // Use a clone so we don't mutate new_program yet
4661        let mut truncated_program = new_program.clone();
4662        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4663            .map_err(KclErrorWithOutputs::no_outputs)?;
4664
4665        // Execute - if this fails, we haven't modified self yet, so state is safe
4666        let outcome = ctx
4667            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4668            .await?;
4669
4670        let new_object_ids = {
4671            // Extract the constraint ID from the execution outcome using source_range_to_object
4672            let constraint_id = outcome
4673                .source_range_to_object
4674                .get(&constraint_node_ref.range)
4675                .copied()
4676                .ok_or_else(|| {
4677                    KclErrorWithOutputs::from_error_outcome(
4678                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4679                        outcome.clone(),
4680                    )
4681                })?;
4682            vec![constraint_id]
4683        };
4684
4685        // Only now, after all operations succeeded, update self.program.
4686        // This ensures state is only modified if everything succeeds.
4687        self.program = new_program;
4688
4689        // Uses MockConfig::default() which has freedom_analysis: true
4690        let outcome = self.update_state_after_exec(outcome, true);
4691
4692        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4693        let scene_graph_delta = SceneGraphDelta {
4694            new_graph: self.scene_graph_for_ui(),
4695            invalidates_ids: false,
4696            new_objects: new_object_ids,
4697            exec_outcome: outcome,
4698        };
4699        Ok((src_delta, scene_graph_delta))
4700    }
4701
4702    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4703        let commit_failure = || {
4704            KclErrorWithOutputs::from_error_outcome(
4705                KclError::refactor(format!("Could not update KCL after {operation}.")),
4706                outcome.clone(),
4707            )
4708        };
4709
4710        let default_length_unit = self.default_length_unit();
4711        let mut settled_ast = self.program.ast.clone();
4712        let mut committed_solver_value = false;
4713        for (var_range, node_path, value) in &outcome.var_solutions {
4714            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4715                return Err(commit_failure());
4716            };
4717            let new_value = match &lookup {
4718                Some(current_literal) => {
4719                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4720                        continue;
4721                    }
4722                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4723                }
4724                None => {
4725                    // Bare `var` with no initial literal to compare against;
4726                    // always commit, using the module's default length unit as
4727                    // an explicit suffix so the written value carries units.
4728                    Number {
4729                        value: number_value_in_default_length_units(*value, default_length_unit),
4730                        units: default_length_unit.into(),
4731                    }
4732                }
4733            };
4734            committed_solver_value = true;
4735            let source_ref = SourceRef::Simple {
4736                range: *var_range,
4737                node_path: node_path.clone(),
4738            };
4739            mutate_ast_node_by_source_ref(
4740                &mut settled_ast,
4741                &source_ref,
4742                AstMutateCommand::EditVarInitialValue { value: new_value },
4743            )
4744            .map_err(|_| commit_failure())?;
4745        }
4746
4747        if !committed_solver_value {
4748            return Ok(SourceDelta {
4749                text: self.program.original_file_contents.clone(),
4750            });
4751        }
4752
4753        let settled_source = source_from_ast(&settled_ast);
4754        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4755        if !errors.is_empty() {
4756            return Err(commit_failure());
4757        }
4758        let Some(settled_program) = settled_program else {
4759            return Err(commit_failure());
4760        };
4761
4762        self.program = settled_program;
4763
4764        Ok(SourceDelta { text: settled_source })
4765    }
4766
4767    // Find constraints that reference the given segments.
4768    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4769        if segment_ids_set.contains(&segment_id) {
4770            return true;
4771        }
4772
4773        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4774            return false;
4775        };
4776        let ObjectKind::Segment { segment } = &segment_object.kind else {
4777            return false;
4778        };
4779        let Segment::Point(point) = segment else {
4780            return false;
4781        };
4782
4783        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4784    }
4785
4786    fn remaining_constraint_segments(
4787        &self,
4788        segments: &[ConstraintSegment],
4789        segment_ids_set: &AhashIndexSet<ObjectId>,
4790    ) -> Vec<ConstraintSegment> {
4791        segments
4792            .iter()
4793            .copied()
4794            .filter(|segment| match segment {
4795                ConstraintSegment::Origin(_) => true,
4796                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4797            })
4798            .collect()
4799    }
4800
4801    fn find_referenced_constraints(
4802        &self,
4803        sketch_id: ObjectId,
4804        segment_ids_set: &AhashIndexSet<ObjectId>,
4805    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4806        // Look up the sketch.
4807        let sketch_object = self
4808            .scene_graph
4809            .objects
4810            .get(sketch_id.0)
4811            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4812        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4813            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4814        };
4815        let segment_or_owner_matches = |segment_id: ObjectId| {
4816            if segment_ids_set.contains(&segment_id) {
4817                return true;
4818            }
4819            let segment_object = self.scene_graph.objects.get(segment_id.0);
4820            if let Some(obj) = segment_object
4821                && let ObjectKind::Segment { segment } = &obj.kind
4822            {
4823                match segment {
4824                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4825                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4826                    _ => false,
4827                }
4828            } else {
4829                false
4830            }
4831        };
4832        let mut constraint_ids_set = AhashIndexSet::default();
4833        for constraint_id in &sketch.constraints {
4834            let constraint_object = self
4835                .scene_graph
4836                .objects
4837                .get(constraint_id.0)
4838                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4839            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4840                return Err(KclError::refactor(format!(
4841                    "Object is not a constraint, it is {}",
4842                    constraint_object.kind.human_friendly_kind_with_article()
4843                )));
4844            };
4845            let depends_on_segment = match constraint {
4846                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4847                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4848                Constraint::Fixed(fixed) => fixed
4849                    .points
4850                    .iter()
4851                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4852                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4853                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4854                Constraint::EqualRadius(equal_radius) => {
4855                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4856                }
4857                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4858                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4859                Constraint::Horizontal(h) => match h {
4860                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4861                    Horizontal::Points { points } => points.iter().any(|point| match point {
4862                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4863                        ConstraintSegment::Origin(_) => false,
4864                    }),
4865                },
4866                Constraint::Vertical(v) => match v {
4867                    Vertical::Line { line } => segment_or_owner_matches(*line),
4868                    Vertical::Points { points } => points.iter().any(|point| match point {
4869                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4870                        ConstraintSegment::Origin(_) => false,
4871                    }),
4872                },
4873                Constraint::LinesEqualLength(lines_equal_length) => {
4874                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4875                }
4876                Constraint::Midpoint(midpoint) => {
4877                    segment_or_owner_matches(midpoint.segment)
4878                        || matches!(
4879                            midpoint.point,
4880                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4881                        )
4882                }
4883                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4884                Constraint::Perpendicular(perpendicular) => {
4885                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4886                }
4887                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4888                Constraint::Symmetric(symmetric) => {
4889                    segment_or_owner_matches(symmetric.axis)
4890                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4891                }
4892                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4893            };
4894            if depends_on_segment {
4895                constraint_ids_set.insert(*constraint_id);
4896            }
4897        }
4898        Ok(constraint_ids_set)
4899    }
4900
4901    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4902        let mut outcome = outcome;
4903        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4904
4905        if freedom_analysis_ran {
4906            // When freedom analysis ran, replace the cache entirely with new values
4907            // Don't merge with old values since IDs might have changed
4908            self.point_freedom_cache.clear();
4909            for new_obj in &new_objects {
4910                if let ObjectKind::Segment {
4911                    segment: crate::front::Segment::Point(point),
4912                } = &new_obj.kind
4913                {
4914                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4915                }
4916            }
4917            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4918            // Objects are already correct from the analysis, just use them as-is
4919            self.scene_graph.objects = new_objects;
4920        } else {
4921            // When freedom analysis didn't run, preserve old values and merge
4922            // Before replacing objects, extract and store freedom values from old objects
4923            for old_obj in &self.scene_graph.objects {
4924                if let ObjectKind::Segment {
4925                    segment: crate::front::Segment::Point(point),
4926                } = &old_obj.kind
4927                {
4928                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4929                }
4930            }
4931
4932            // Update objects, preserving stored freedom values when new is Free (might be default)
4933            let mut updated_objects = Vec::with_capacity(new_objects.len());
4934            for new_obj in new_objects {
4935                let mut obj = new_obj;
4936                if let ObjectKind::Segment {
4937                    segment: crate::front::Segment::Point(point),
4938                } = &mut obj.kind
4939                {
4940                    let new_freedom = point.freedom;
4941                    // When freedom_analysis=false, new values are defaults (Free).
4942                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4943                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4944                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4945                    // when there are actually unsatisfied constraints.
4946                    match new_freedom {
4947                        Freedom::Free => {
4948                            match self.point_freedom_cache.get(&obj.id).copied() {
4949                                Some(Freedom::Conflict) => {
4950                                    // Don't preserve Conflict - conflicts are transient
4951                                    // Keep it as Free
4952                                }
4953                                Some(Freedom::Fixed) => {
4954                                    // Preserve Fixed cached value
4955                                    point.freedom = Freedom::Fixed;
4956                                }
4957                                Some(Freedom::Free) => {
4958                                    // If stored is also Free, keep Free (no change needed)
4959                                }
4960                                None => {
4961                                    // If no cached value, keep Free (default)
4962                                }
4963                            }
4964                        }
4965                        Freedom::Fixed => {
4966                            // Use new value (already set)
4967                        }
4968                        Freedom::Conflict => {
4969                            // Use new value (already set)
4970                        }
4971                    }
4972                    // Store the new freedom value (even if it's Free, so we know it was set)
4973                    self.point_freedom_cache.insert(obj.id, point.freedom);
4974                }
4975                updated_objects.push(obj);
4976            }
4977
4978            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4979            self.scene_graph.objects = updated_objects;
4980        }
4981        outcome
4982    }
4983
4984    fn mutate_ast(
4985        &mut self,
4986        ast: &mut ast::Node<ast::Program>,
4987        object_id: ObjectId,
4988        command: AstMutateCommand,
4989    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4990        let sketch_object = self
4991            .scene_graph
4992            .objects
4993            .get(object_id.0)
4994            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4995        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4996    }
4997}
4998
4999fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5000    // Look up existing sketch.
5001    let sketch_object = scene_graph
5002        .objects
5003        .get(sketch_id.0)
5004        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5005    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5006        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5007    };
5008    expect_single_node_ref(sketch_object)
5009}
5010
5011fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5012    match &object.source {
5013        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5014            range: *range,
5015            node_path: node_path.clone(),
5016        }),
5017        SourceRef::BackTrace { ranges } => {
5018            let [range] = ranges.as_slice() else {
5019                return Err(KclError::refactor(format!(
5020                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5021                    ranges.len()
5022                )));
5023            };
5024            Ok(AstNodeRef {
5025                range: range.0,
5026                node_path: range.1.clone(),
5027            })
5028        }
5029    }
5030}
5031
5032/// This is a deprecated fall-back implementation. Prefer
5033/// [`only_sketch_block()`] to avoid reliance on source ranges.
5034fn only_sketch_block_from_range(
5035    ast: &mut ast::Node<ast::Program>,
5036    sketch_block_range: SourceRange,
5037    edit_kind: ChangeKind,
5038) -> Result<(), KclError> {
5039    let r1 = sketch_block_range;
5040    let matches_range = |r2: SourceRange| -> bool {
5041        // We may have added items to the sketch block, so the end may not be an
5042        // exact match.
5043        match edit_kind {
5044            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5045            // For edit, we don't know whether it grew or shrank.
5046            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5047            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5048            // No edit should be an exact match.
5049            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5050        }
5051    };
5052    let mut found = false;
5053    for item in ast.body.iter_mut() {
5054        match item {
5055            ast::BodyItem::ImportStatement(_) => {}
5056            ast::BodyItem::ExpressionStatement(node) => {
5057                if matches_range(SourceRange::from(&*node))
5058                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5059                {
5060                    sketch_block.is_being_edited = true;
5061                    found = true;
5062                    break;
5063                }
5064            }
5065            ast::BodyItem::VariableDeclaration(node) => {
5066                if matches_range(SourceRange::from(&node.declaration.init))
5067                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5068                {
5069                    sketch_block.is_being_edited = true;
5070                    found = true;
5071                    break;
5072                }
5073            }
5074            ast::BodyItem::TypeDeclaration(_) => {}
5075            ast::BodyItem::ReturnStatement(node) => {
5076                if matches_range(SourceRange::from(&node.argument))
5077                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5078                {
5079                    sketch_block.is_being_edited = true;
5080                    found = true;
5081                    break;
5082                }
5083            }
5084        }
5085    }
5086    if !found {
5087        return Err(KclError::refactor(format!(
5088            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5089        )));
5090    }
5091
5092    Ok(())
5093}
5094
5095fn only_sketch_block(
5096    ast: &mut ast::Node<ast::Program>,
5097    sketch_block_ref: &AstNodeRef,
5098    edit_kind: ChangeKind,
5099) -> Result<(), KclError> {
5100    let Some(target_node_path) = &sketch_block_ref.node_path else {
5101        #[cfg(target_arch = "wasm32")]
5102        web_sys::console::warn_1(
5103            &format!(
5104                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5105                &sketch_block_ref
5106            )
5107            .into(),
5108        );
5109        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5110    };
5111    let mut found = false;
5112    for item in ast.body.iter_mut() {
5113        match item {
5114            ast::BodyItem::ImportStatement(_) => {}
5115            ast::BodyItem::ExpressionStatement(node) => {
5116                // Check the statement.
5117                if let Some(node_path) = &node.node_path
5118                    && node_path == target_node_path
5119                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5120                {
5121                    sketch_block.is_being_edited = true;
5122                    found = true;
5123                    break;
5124                }
5125                // Check the expression.
5126                if let Some(node_path) = node.expression.node_path()
5127                    && node_path == target_node_path
5128                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5129                {
5130                    sketch_block.is_being_edited = true;
5131                    found = true;
5132                    break;
5133                }
5134            }
5135            ast::BodyItem::VariableDeclaration(node) => {
5136                if let Some(node_path) = node.declaration.init.node_path()
5137                    && node_path == target_node_path
5138                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5139                {
5140                    sketch_block.is_being_edited = true;
5141                    found = true;
5142                    break;
5143                }
5144            }
5145            ast::BodyItem::TypeDeclaration(_) => {}
5146            ast::BodyItem::ReturnStatement(node) => {
5147                if let Some(node_path) = node.argument.node_path()
5148                    && node_path == target_node_path
5149                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5150                {
5151                    sketch_block.is_being_edited = true;
5152                    found = true;
5153                    break;
5154                }
5155            }
5156        }
5157    }
5158    if !found {
5159        return Err(KclError::refactor(format!(
5160            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5161        )));
5162    }
5163
5164    Ok(())
5165}
5166
5167fn sketch_on_ast_expr(
5168    ast: &mut ast::Node<ast::Program>,
5169    scene_graph: &SceneGraph,
5170    on: &Plane,
5171) -> Result<ast::Expr, KclError> {
5172    match on {
5173        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5174        Plane::Object(object_id) => {
5175            let on_object = scene_graph
5176                .objects
5177                .get(object_id.0)
5178                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5179            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5180                return Ok(face_expr);
5181            }
5182            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5183        }
5184    }
5185}
5186
5187fn sketch_face_of_scene_object_ast_expr(
5188    ast: &mut ast::Node<ast::Program>,
5189    on_object: &crate::front::Object,
5190) -> Result<Option<ast::Expr>, KclError> {
5191    let SourceRef::BackTrace { ranges } = &on_object.source else {
5192        return Ok(None);
5193    };
5194
5195    match &on_object.kind {
5196        ObjectKind::Wall(_) => {
5197            let [sweep_range, segment_range] = ranges.as_slice() else {
5198                return Err(KclError::refactor(format!(
5199                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5200                    ranges.len(),
5201                    on_object.artifact_id
5202                )));
5203            };
5204            let sweep_ref = get_or_insert_ast_reference(
5205                ast,
5206                &SourceRef::Simple {
5207                    range: sweep_range.0,
5208                    node_path: sweep_range.1.clone(),
5209                },
5210                "solid",
5211                None,
5212            )?;
5213            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5214                return Err(KclError::refactor(format!(
5215                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5216                    on_object.artifact_id
5217                )));
5218            };
5219            let solid_name = solid_name_expr.name.name.clone();
5220            let solid_expr = ast_name_expr(solid_name.clone());
5221            let segment_ref = get_or_insert_ast_reference(
5222                ast,
5223                &SourceRef::Simple {
5224                    range: segment_range.0,
5225                    node_path: segment_range.1.clone(),
5226                },
5227                LINE_VARIABLE,
5228                None,
5229            )?;
5230
5231            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5232                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5233                    return Err(KclError::refactor(format!(
5234                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5235                        on_object.artifact_id
5236                    )));
5237                };
5238                create_member_expression(
5239                    create_member_expression(ast_name_expr(region_name), "tags"),
5240                    &segment_name_expr.name.name,
5241                )
5242            } else {
5243                segment_ref
5244            };
5245
5246            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5247        }
5248        ObjectKind::Cap(cap) => {
5249            let [range] = ranges.as_slice() else {
5250                return Err(KclError::refactor(format!(
5251                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5252                    ranges.len(),
5253                    on_object.artifact_id
5254                )));
5255            };
5256            let sweep_ref = get_or_insert_ast_reference(
5257                ast,
5258                &SourceRef::Simple {
5259                    range: range.0,
5260                    node_path: range.1.clone(),
5261                },
5262                "solid",
5263                None,
5264            )?;
5265            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5266                return Err(KclError::refactor(format!(
5267                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5268                    on_object.artifact_id
5269                )));
5270            };
5271            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5272            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5273            let face_expr = match cap.kind {
5274                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5275                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5276            };
5277
5278            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5279        }
5280        _ => Ok(None),
5281    }
5282}
5283
5284fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5285    let mut existing_artifact_ids = scene_objects
5286        .iter()
5287        .map(|object| object.artifact_id)
5288        .collect::<HashSet<_>>();
5289
5290    for artifact in artifact_graph.values() {
5291        match artifact {
5292            Artifact::Wall(wall) => {
5293                if existing_artifact_ids.contains(&wall.id) {
5294                    continue;
5295                }
5296
5297                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5298                    Artifact::Segment(segment) => Some(segment),
5299                    _ => None,
5300                }) else {
5301                    continue;
5302                };
5303                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5304                    Artifact::Sweep(sweep) => Some(sweep),
5305                    _ => None,
5306                }) else {
5307                    continue;
5308                };
5309                let source_segment = segment
5310                    .original_seg_id
5311                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5312                    .and_then(|artifact| match artifact {
5313                        Artifact::Segment(segment) => Some(segment),
5314                        _ => None,
5315                    })
5316                    .unwrap_or(segment);
5317                let id = ObjectId(scene_objects.len());
5318                scene_objects.push(crate::front::Object {
5319                    id,
5320                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5321                    label: Default::default(),
5322                    comments: Default::default(),
5323                    artifact_id: wall.id,
5324                    source: SourceRef::BackTrace {
5325                        ranges: vec![
5326                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5327                            (
5328                                source_segment.code_ref.range,
5329                                Some(source_segment.code_ref.node_path.clone()),
5330                            ),
5331                        ],
5332                    },
5333                });
5334                existing_artifact_ids.insert(wall.id);
5335            }
5336            Artifact::Cap(cap) => {
5337                if existing_artifact_ids.contains(&cap.id) {
5338                    continue;
5339                }
5340
5341                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5342                    Artifact::Sweep(sweep) => Some(sweep),
5343                    _ => None,
5344                }) else {
5345                    continue;
5346                };
5347                let id = ObjectId(scene_objects.len());
5348                let kind = match cap.sub_type {
5349                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5350                    CapSubType::End => crate::frontend::api::CapKind::End,
5351                };
5352                scene_objects.push(crate::front::Object {
5353                    id,
5354                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5355                    label: Default::default(),
5356                    comments: Default::default(),
5357                    artifact_id: cap.id,
5358                    source: SourceRef::BackTrace {
5359                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5360                    },
5361                });
5362                existing_artifact_ids.insert(cap.id);
5363            }
5364            _ => {}
5365        }
5366    }
5367}
5368
5369fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5370    use crate::engine::PlaneName;
5371
5372    match name {
5373        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5374        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5375        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5376        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5377        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5378        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5379    }
5380}
5381
5382fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5383    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5384        ast::UnaryOperator::Neg,
5385        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5386    )))
5387}
5388
5389fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5390    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5391        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5392        unlabeled: Some(solid_expr),
5393        arguments: vec![ast::LabeledArg {
5394            label: Some(ast::Identifier::new("face")),
5395            arg: face_expr,
5396        }],
5397        digest: None,
5398        non_code_meta: Default::default(),
5399    })))
5400}
5401
5402fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5403    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5404        return None;
5405    };
5406    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5407        return None;
5408    };
5409    if !matches!(
5410        sweep_call.callee.name.name.as_str(),
5411        "extrude" | "revolve" | "sweep" | "loft"
5412    ) {
5413        return None;
5414    }
5415    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5416        return None;
5417    };
5418    let candidate = region_name_expr.name.name.clone();
5419    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5420        return None;
5421    };
5422    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5423        return None;
5424    };
5425    if region_call.callee.name.name != "region" {
5426        return None;
5427    }
5428    Some(candidate)
5429}
5430
5431/// Return the AST expression referencing the variable at the given source ref.
5432/// If no such variable exists, insert a new variable declaration with the given
5433/// prefix.
5434///
5435/// This may return a complex expression referencing properties of the variable
5436/// (e.g., `line1.start`).
5437fn get_or_insert_ast_reference(
5438    ast: &mut ast::Node<ast::Program>,
5439    source_ref: &SourceRef,
5440    prefix: &str,
5441    property: Option<&str>,
5442) -> Result<ast::Expr, KclError> {
5443    let command = AstMutateCommand::AddVariableDeclaration {
5444        prefix: prefix.to_owned(),
5445    };
5446    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5447    let AstMutateCommandReturn::Name(var_name) = ret else {
5448        return Err(KclError::refactor(
5449            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5450        ));
5451    };
5452    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5453    let Some(property) = property else {
5454        // No property; just return the variable name.
5455        return Ok(var_expr);
5456    };
5457
5458    Ok(create_member_expression(var_expr, property))
5459}
5460
5461fn mutate_ast_node_by_source_ref(
5462    ast: &mut ast::Node<ast::Program>,
5463    source_ref: &SourceRef,
5464    command: AstMutateCommand,
5465) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5466    let (source_range, node_path) = match source_ref {
5467        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5468        SourceRef::BackTrace { ranges } => {
5469            let [range] = ranges.as_slice() else {
5470                return Err(KclError::refactor(format!(
5471                    "Expected single source ref, got {}; ranges={ranges:#?}",
5472                    ranges.len(),
5473                )));
5474            };
5475            (range.0, range.1.clone())
5476        }
5477    };
5478    let mut context = AstMutateContext {
5479        source_range,
5480        node_path,
5481        command,
5482        defined_names_stack: Default::default(),
5483    };
5484    let control = dfs_mut(ast, &mut context);
5485    match control {
5486        ControlFlow::Continue(_) => Err(KclError::refactor(
5487            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5488        )),
5489        ControlFlow::Break(break_value) => break_value,
5490    }
5491}
5492
5493#[derive(Debug)]
5494struct AstMutateContext {
5495    source_range: SourceRange,
5496    node_path: Option<ast::NodePath>,
5497    command: AstMutateCommand,
5498    defined_names_stack: Vec<HashSet<String>>,
5499}
5500
5501#[derive(Debug)]
5502#[allow(clippy::large_enum_variant)]
5503enum AstMutateCommand {
5504    /// Add an expression statement to the sketch block.
5505    AddSketchBlockExprStmt {
5506        expr: ast::Expr,
5507    },
5508    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5509    AddSketchBlockVarDecl {
5510        prefix: String,
5511        expr: ast::Expr,
5512    },
5513    AddVariableDeclaration {
5514        prefix: String,
5515    },
5516    EditPoint {
5517        at: ast::Expr,
5518    },
5519    EditLine {
5520        start: ast::Expr,
5521        end: ast::Expr,
5522        construction: Option<bool>,
5523    },
5524    EditArc {
5525        start: ast::Expr,
5526        end: ast::Expr,
5527        center: ast::Expr,
5528        construction: Option<bool>,
5529    },
5530    EditCircle {
5531        start: ast::Expr,
5532        center: ast::Expr,
5533        construction: Option<bool>,
5534    },
5535    EditControlPointSpline {
5536        points: ast::Expr,
5537        construction: Option<bool>,
5538    },
5539    EditConstraintValue {
5540        value: ast::BinaryPart,
5541    },
5542    EditDistanceConstraintLabelPosition {
5543        label_position: ast::Expr,
5544    },
5545    EditCallUnlabeled {
5546        arg: ast::Expr,
5547    },
5548    EditVarInitialValue {
5549        value: Number,
5550    },
5551    DeleteNode,
5552}
5553
5554impl AstMutateCommand {
5555    fn needs_defined_names_stack(&self) -> bool {
5556        matches!(
5557            self,
5558            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5559        )
5560    }
5561}
5562
5563#[derive(Debug)]
5564enum AstMutateCommandReturn {
5565    None,
5566    Name(String),
5567}
5568
5569#[derive(Debug, Clone)]
5570struct AstNodeRef {
5571    range: SourceRange,
5572    node_path: Option<ast::NodePath>,
5573}
5574
5575impl<T> From<&ast::Node<T>> for AstNodeRef {
5576    fn from(value: &ast::Node<T>) -> Self {
5577        AstNodeRef {
5578            range: value.into(),
5579            node_path: value.node_path.clone(),
5580        }
5581    }
5582}
5583
5584impl From<&ast::BodyItem> for AstNodeRef {
5585    fn from(value: &ast::BodyItem) -> Self {
5586        match value {
5587            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5588                range: node.into(),
5589                node_path: node.node_path.clone(),
5590            },
5591            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5592                range: node.into(),
5593                node_path: node.node_path.clone(),
5594            },
5595            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5596                range: node.into(),
5597                node_path: node.node_path.clone(),
5598            },
5599            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5600                range: node.into(),
5601                node_path: node.node_path.clone(),
5602            },
5603            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5604                range: node.into(),
5605                node_path: node.node_path.clone(),
5606            },
5607        }
5608    }
5609}
5610
5611impl From<&ast::Expr> for AstNodeRef {
5612    fn from(value: &ast::Expr) -> Self {
5613        AstNodeRef {
5614            range: SourceRange::from(value),
5615            node_path: value.node_path().cloned(),
5616        }
5617    }
5618}
5619
5620impl From<&AstMutateContext> for AstNodeRef {
5621    fn from(value: &AstMutateContext) -> Self {
5622        AstNodeRef {
5623            range: value.source_range,
5624            node_path: value.node_path.clone(),
5625        }
5626    }
5627}
5628
5629impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5630    type Error = crate::walk::AstNodeError;
5631
5632    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5633        Ok(AstNodeRef {
5634            range: SourceRange::try_from(value)?,
5635            node_path: value.try_into()?,
5636        })
5637    }
5638}
5639
5640impl From<AstNodeRef> for SourceRange {
5641    fn from(value: AstNodeRef) -> Self {
5642        value.range
5643    }
5644}
5645
5646impl Visitor for AstMutateContext {
5647    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5648    type Continue = ();
5649
5650    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5651        filter_and_process(self, node)
5652    }
5653
5654    fn finish(&mut self, node: NodeMut<'_>) {
5655        match &node {
5656            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5657                self.defined_names_stack.pop();
5658            }
5659            _ => {}
5660        }
5661    }
5662}
5663
5664fn filter_and_process(
5665    ctx: &mut AstMutateContext,
5666    node: NodeMut,
5667) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5668    let Ok(node_range) = SourceRange::try_from(&node) else {
5669        // Nodes that can't be converted to a range aren't interesting.
5670        return TraversalReturn::new_continue(());
5671    };
5672    // If we're adding a variable declaration, we need to look at variable
5673    // declaration expressions to see if it already has a variable, before
5674    // continuing. The variable declaration's source range won't match the
5675    // target; its init expression will.
5676    if let NodeMut::VariableDeclaration(var_decl) = &node {
5677        let expr_range = SourceRange::from(&var_decl.declaration.init);
5678        let expr_node_path = var_decl.declaration.init.node_path();
5679        if source_ref_matches(ctx, expr_range, expr_node_path) {
5680            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5681                // We found the variable declaration expression. It doesn't need
5682                // to be added.
5683                return TraversalReturn::new_break(Ok((
5684                    AstNodeRef::from(&**var_decl),
5685                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5686                )));
5687            }
5688            if let AstMutateCommand::DeleteNode = &ctx.command {
5689                // We found the variable declaration. Delete the variable along
5690                // with the segment.
5691                return TraversalReturn {
5692                    mutate_body_item: MutateBodyItem::Delete,
5693                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5694                };
5695            }
5696        }
5697    }
5698    // Similar thing with expression statement. We need to look at the
5699    // expression inside it.
5700    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5701        let expr_range = SourceRange::from(&expr_stmt.expression);
5702        let expr_node_path = expr_stmt.expression.node_path();
5703        if source_ref_matches(ctx, expr_range, expr_node_path) {
5704            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5705                // We found the node wrapped in an expression statement. Process
5706                // the statement.
5707                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5708                    return TraversalReturn::new_continue(());
5709                };
5710                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5711            }
5712            if let AstMutateCommand::DeleteNode = &ctx.command {
5713                // We found the node wrapped in an expression statement. Delete
5714                // the whole statement.
5715                return TraversalReturn {
5716                    mutate_body_item: MutateBodyItem::Delete,
5717                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5718                };
5719            }
5720        }
5721    }
5722
5723    if ctx.command.needs_defined_names_stack() {
5724        if let NodeMut::Program(program) = &node {
5725            ctx.defined_names_stack.push(find_defined_names(*program));
5726        } else if let NodeMut::SketchBlock(block) = &node {
5727            ctx.defined_names_stack.push(find_defined_names(&block.body));
5728        }
5729    }
5730
5731    // Make sure the node matches the source ref.
5732    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5733    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5734        return TraversalReturn::new_continue(());
5735    }
5736    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5737        return TraversalReturn::new_continue(());
5738    };
5739    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5740}
5741
5742fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5743    match &ctx.node_path {
5744        Some(target) => Some(target) == node_path,
5745        None => node_range == ctx.source_range,
5746    }
5747}
5748
5749fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5750    match &ctx.command {
5751        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5752            if let NodeMut::SketchBlock(sketch_block) = node {
5753                sketch_block
5754                    .body
5755                    .items
5756                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5757                        inner: ast::ExpressionStatement {
5758                            expression: expr.clone(),
5759                            digest: None,
5760                        },
5761                        start: Default::default(),
5762                        end: Default::default(),
5763                        module_id: Default::default(),
5764                        node_path: None,
5765                        outer_attrs: Default::default(),
5766                        pre_comments: Default::default(),
5767                        comment_start: Default::default(),
5768                    }));
5769                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5770            }
5771        }
5772        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5773            if let NodeMut::SketchBlock(sketch_block) = node {
5774                let empty_defined_names = HashSet::new();
5775                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5776                let Ok(name) = next_free_name(prefix, defined_names) else {
5777                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5778                };
5779                sketch_block
5780                    .body
5781                    .items
5782                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5783                        ast::VariableDeclaration::new(
5784                            ast::VariableDeclarator::new(&name, expr.clone()),
5785                            ast::ItemVisibility::Default,
5786                            ast::VariableKind::Const,
5787                        ),
5788                    ))));
5789                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5790            }
5791        }
5792        AstMutateCommand::AddVariableDeclaration { prefix } => {
5793            if let NodeMut::VariableDeclaration(inner) = node {
5794                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5795            }
5796            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5797                let empty_defined_names = HashSet::new();
5798                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5799                let Ok(name) = next_free_name(prefix, defined_names) else {
5800                    // TODO: Return an error instead?
5801                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5802                };
5803                let mutate_node =
5804                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5805                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5806                        ast::ItemVisibility::Default,
5807                        ast::VariableKind::Const,
5808                    ))));
5809                return TraversalReturn {
5810                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5811                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5812                };
5813            }
5814        }
5815        AstMutateCommand::EditPoint { at } => {
5816            if let NodeMut::CallExpressionKw(call) = node {
5817                if call.callee.name.name != POINT_FN {
5818                    return TraversalReturn::new_continue(());
5819                }
5820                // Update the arguments.
5821                for labeled_arg in &mut call.arguments {
5822                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5823                        labeled_arg.arg = at.clone();
5824                    }
5825                }
5826                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5827            }
5828        }
5829        AstMutateCommand::EditLine {
5830            start,
5831            end,
5832            construction,
5833        } => {
5834            if let NodeMut::CallExpressionKw(call) = node {
5835                if call.callee.name.name != LINE_FN {
5836                    return TraversalReturn::new_continue(());
5837                }
5838                // Update the arguments.
5839                for labeled_arg in &mut call.arguments {
5840                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5841                        labeled_arg.arg = start.clone();
5842                    }
5843                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5844                        labeled_arg.arg = end.clone();
5845                    }
5846                }
5847                // Handle construction kwarg
5848                if let Some(construction_value) = construction {
5849                    let construction_exists = call
5850                        .arguments
5851                        .iter()
5852                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5853                    if *construction_value {
5854                        // Add or update construction=true
5855                        if construction_exists {
5856                            // Update existing construction kwarg
5857                            for labeled_arg in &mut call.arguments {
5858                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5859                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5860                                        value: ast::LiteralValue::Bool(true),
5861                                        raw: "true".to_string(),
5862                                        digest: None,
5863                                    })));
5864                                }
5865                            }
5866                        } else {
5867                            // Add new construction kwarg
5868                            call.arguments.push(ast::LabeledArg {
5869                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5870                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5871                                    value: ast::LiteralValue::Bool(true),
5872                                    raw: "true".to_string(),
5873                                    digest: None,
5874                                }))),
5875                            });
5876                        }
5877                    } else {
5878                        // Remove construction kwarg if it exists
5879                        call.arguments
5880                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5881                    }
5882                }
5883                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5884            }
5885        }
5886        AstMutateCommand::EditArc {
5887            start,
5888            end,
5889            center,
5890            construction,
5891        } => {
5892            if let NodeMut::CallExpressionKw(call) = node {
5893                if call.callee.name.name != ARC_FN {
5894                    return TraversalReturn::new_continue(());
5895                }
5896                // Update the arguments.
5897                for labeled_arg in &mut call.arguments {
5898                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5899                        labeled_arg.arg = start.clone();
5900                    }
5901                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5902                        labeled_arg.arg = end.clone();
5903                    }
5904                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5905                        labeled_arg.arg = center.clone();
5906                    }
5907                }
5908                // Handle construction kwarg
5909                if let Some(construction_value) = construction {
5910                    let construction_exists = call
5911                        .arguments
5912                        .iter()
5913                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5914                    if *construction_value {
5915                        // Add or update construction=true
5916                        if construction_exists {
5917                            // Update existing construction kwarg
5918                            for labeled_arg in &mut call.arguments {
5919                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5920                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5921                                        value: ast::LiteralValue::Bool(true),
5922                                        raw: "true".to_string(),
5923                                        digest: None,
5924                                    })));
5925                                }
5926                            }
5927                        } else {
5928                            // Add new construction kwarg
5929                            call.arguments.push(ast::LabeledArg {
5930                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5931                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5932                                    value: ast::LiteralValue::Bool(true),
5933                                    raw: "true".to_string(),
5934                                    digest: None,
5935                                }))),
5936                            });
5937                        }
5938                    } else {
5939                        // Remove construction kwarg if it exists
5940                        call.arguments
5941                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5942                    }
5943                }
5944                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5945            }
5946        }
5947        AstMutateCommand::EditCircle {
5948            start,
5949            center,
5950            construction,
5951        } => {
5952            if let NodeMut::CallExpressionKw(call) = node {
5953                if call.callee.name.name != CIRCLE_FN {
5954                    return TraversalReturn::new_continue(());
5955                }
5956                // Update the arguments.
5957                for labeled_arg in &mut call.arguments {
5958                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5959                        labeled_arg.arg = start.clone();
5960                    }
5961                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5962                        labeled_arg.arg = center.clone();
5963                    }
5964                }
5965                // Handle construction kwarg
5966                if let Some(construction_value) = construction {
5967                    let construction_exists = call
5968                        .arguments
5969                        .iter()
5970                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5971                    if *construction_value {
5972                        if construction_exists {
5973                            // Update existing construction kwarg
5974                            for labeled_arg in &mut call.arguments {
5975                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5976                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5977                                        value: ast::LiteralValue::Bool(true),
5978                                        raw: "true".to_string(),
5979                                        digest: None,
5980                                    })));
5981                                }
5982                            }
5983                        } else {
5984                            // Add new construction kwarg
5985                            call.arguments.push(ast::LabeledArg {
5986                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5987                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5988                                    value: ast::LiteralValue::Bool(true),
5989                                    raw: "true".to_string(),
5990                                    digest: None,
5991                                }))),
5992                            });
5993                        }
5994                    } else {
5995                        // Remove construction kwarg if it exists
5996                        call.arguments
5997                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5998                    }
5999                }
6000                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6001            }
6002        }
6003        AstMutateCommand::EditControlPointSpline { points, construction } => {
6004            if let NodeMut::CallExpressionKw(call) = node {
6005                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6006                    return TraversalReturn::new_continue(());
6007                }
6008                for labeled_arg in &mut call.arguments {
6009                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6010                    {
6011                        labeled_arg.arg = points.clone();
6012                    }
6013                }
6014                // Handle construction kwarg
6015                if let Some(construction_value) = construction {
6016                    let construction_exists = call
6017                        .arguments
6018                        .iter()
6019                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6020                    if *construction_value {
6021                        if construction_exists {
6022                            for labeled_arg in &mut call.arguments {
6023                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6024                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6025                                        value: ast::LiteralValue::Bool(true),
6026                                        raw: "true".to_string(),
6027                                        digest: None,
6028                                    })));
6029                                }
6030                            }
6031                        } else {
6032                            call.arguments.push(ast::LabeledArg {
6033                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6034                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6035                                    value: ast::LiteralValue::Bool(true),
6036                                    raw: "true".to_string(),
6037                                    digest: None,
6038                                }))),
6039                            });
6040                        }
6041                    } else {
6042                        call.arguments
6043                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6044                    }
6045                }
6046                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6047            }
6048        }
6049        AstMutateCommand::EditConstraintValue { value } => {
6050            if let NodeMut::BinaryExpression(binary_expr) = node {
6051                let left_is_constraint = matches!(
6052                    &binary_expr.left,
6053                    ast::BinaryPart::CallExpressionKw(call)
6054                        if matches!(
6055                            call.callee.name.name.as_str(),
6056                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6057                        )
6058                );
6059                if left_is_constraint {
6060                    binary_expr.right = value.clone();
6061                } else {
6062                    binary_expr.left = value.clone();
6063                }
6064
6065                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6066            }
6067        }
6068        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6069            if let NodeMut::BinaryExpression(binary_expr) = node {
6070                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6071                    return TraversalReturn::new_continue(());
6072                };
6073                if !matches!(
6074                    call.callee.name.name.as_str(),
6075                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6076                ) {
6077                    return TraversalReturn::new_continue(());
6078                }
6079
6080                if let Some(label_arg) = call
6081                    .arguments
6082                    .iter_mut()
6083                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6084                {
6085                    label_arg.arg = label_position.clone();
6086                } else {
6087                    call.arguments.push(ast::LabeledArg {
6088                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6089                        arg: label_position.clone(),
6090                    });
6091                }
6092
6093                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6094            }
6095        }
6096        AstMutateCommand::EditCallUnlabeled { arg } => {
6097            if let NodeMut::CallExpressionKw(call) = node {
6098                call.unlabeled = Some(arg.clone());
6099                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6100            }
6101        }
6102        AstMutateCommand::EditVarInitialValue { value } => {
6103            // We target the SketchVar itself (matched by NodePath) rather than
6104            // the inner NumericLiteral so we can also write back into vars that
6105            // were declared without an initial value (e.g. bare `var`).
6106            if let NodeMut::SketchVar(sketch_var) = node {
6107                let Ok(literal) = to_source_number(*value) else {
6108                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6109                        "Could not convert number to AST literal: {:?}",
6110                        *value
6111                    ))));
6112                };
6113                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6114                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6115            }
6116        }
6117        AstMutateCommand::DeleteNode => {
6118            return TraversalReturn {
6119                mutate_body_item: MutateBodyItem::Delete,
6120                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6121            };
6122        }
6123    }
6124    TraversalReturn::new_continue(())
6125}
6126
6127struct FindSketchBlockSourceRange {
6128    /// The source range of the sketch block before mutation.
6129    target_before_mutation: SourceRange,
6130    /// The source range of the sketch block's last body item after mutation. We
6131    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6132    /// shared reference.
6133    found: Cell<Option<AstNodeRef>>,
6134}
6135
6136impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6137    type Error = crate::front::Error;
6138
6139    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6140        let Ok(node_range) = SourceRange::try_from(&node) else {
6141            return Ok(true);
6142        };
6143
6144        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6145            if node_range.module_id() == self.target_before_mutation.module_id()
6146                && node_range.start() == self.target_before_mutation.start()
6147                // End shouldn't match since we added something.
6148                && node_range.end() >= self.target_before_mutation.end()
6149            {
6150                self.found.set(sketch_block.body.items.last().map(|item| match item {
6151                    // For declarations like `circle1 = circle(...)`, use
6152                    // the init expression range so lookup in source_range_to_object
6153                    // matches the segment source range.
6154                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6155                    _ => AstNodeRef::from(item),
6156                }));
6157                return Ok(false);
6158            } else {
6159                // We found a different sketch block. No need to descend into
6160                // its children since sketch blocks cannot be nested.
6161                return Ok(true);
6162            }
6163        }
6164
6165        for child in node.children().iter() {
6166            if !child.visit(*self)? {
6167                return Ok(false);
6168            }
6169        }
6170
6171        Ok(true)
6172    }
6173}
6174
6175struct FindSketchBlockByNodePath {
6176    /// The Node Path of the sketch block before mutation.
6177    target_node_path: ast::NodePath,
6178    /// The ref of the sketch block's last body item after mutation. We need to
6179    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6180    /// reference.
6181    found: Cell<Option<AstNodeRef>>,
6182}
6183
6184impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6185    type Error = crate::front::Error;
6186
6187    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6188        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6189            return Ok(true);
6190        };
6191
6192        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6193            if let Some(node_path) = node_path
6194                && node_path == self.target_node_path
6195            {
6196                self.found.set(sketch_block.body.items.last().map(|item| match item {
6197                    // For declarations like `circle1 = circle(...)`, use
6198                    // the init expression range so lookup in source_range_to_object
6199                    // matches the segment source range.
6200                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6201                    _ => AstNodeRef::from(item),
6202                }));
6203
6204                return Ok(false);
6205            } else {
6206                // We found a different sketch block. No need to descend into
6207                // its children since sketch blocks cannot be nested.
6208                return Ok(true);
6209            }
6210        }
6211
6212        for child in node.children().iter() {
6213            if !child.visit(*self)? {
6214                return Ok(false);
6215            }
6216        }
6217
6218        Ok(true)
6219    }
6220}
6221
6222/// After adding an item to a sketch block, find the sketch block, and get the
6223/// source range of the added item. We assume that the added item is the last
6224/// item in the sketch block and that the sketch block's source range has grown,
6225/// but not moved from its starting offset.
6226///
6227/// TODO: Do we need to format *before* mutation in case formatting moves the
6228/// sketch block forward?
6229fn find_sketch_block_added_item(
6230    ast: &ast::Node<ast::Program>,
6231    sketch_block_before_mutation: &AstNodeRef,
6232) -> Result<AstNodeRef, KclError> {
6233    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6234        let find = FindSketchBlockByNodePath {
6235            target_node_path: node_path.clone(),
6236            found: Cell::new(None),
6237        };
6238        let node = crate::walk::Node::from(ast);
6239        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6240        find.found.into_inner().ok_or_else(|| {
6241            KclError::refactor(format!(
6242                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6243            ))
6244        })
6245    } else {
6246        // No NodePath. Fall back to legacy source range.
6247        let find = FindSketchBlockSourceRange {
6248            target_before_mutation: sketch_block_before_mutation.range,
6249            found: Cell::new(None),
6250        };
6251        let node = crate::walk::Node::from(ast);
6252        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6253        find.found.into_inner().ok_or_else(|| KclError::refactor(
6254            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?"),
6255        ))
6256    }
6257}
6258
6259fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6260    // TODO: Don't duplicate this from lib.rs Program.
6261    ast.recast_top(&Default::default(), 0)
6262}
6263
6264struct FindNumericLiteral {
6265    target: SourceRange,
6266    found: Cell<Option<ast::NumericLiteral>>,
6267}
6268
6269impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6270    type Error = crate::front::Error;
6271
6272    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6273        let Ok(node_range) = SourceRange::try_from(&node) else {
6274            return Ok(true);
6275        };
6276
6277        if node_range == self.target
6278            && let crate::walk::Node::NumericLiteral(literal) = node
6279        {
6280            self.found.set(Some(literal.inner.clone()));
6281            return Ok(false);
6282        }
6283
6284        for child in node.children().iter() {
6285            if !child.visit(*self)? {
6286                return Ok(false);
6287            }
6288        }
6289
6290        Ok(true)
6291    }
6292}
6293
6294fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6295    let find = FindNumericLiteral {
6296        target,
6297        found: Cell::new(None),
6298    };
6299    let node = crate::walk::Node::from(ast);
6300    node.visit(&find).ok()?;
6301    find.found.into_inner()
6302}
6303
6304struct FindSketchVarInitialByNodePath<'a> {
6305    target: &'a ast::NodePath,
6306    sketch_var_found: Cell<bool>,
6307    initial_literal: Cell<Option<ast::NumericLiteral>>,
6308}
6309
6310impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6311    type Error = crate::front::Error;
6312
6313    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6314        if let crate::walk::Node::SketchVar(sketch_var) = node
6315            && sketch_var.node_path.as_ref() == Some(self.target)
6316        {
6317            self.sketch_var_found.set(true);
6318            if let Some(initial) = &sketch_var.initial {
6319                self.initial_literal.set(Some(initial.inner.clone()));
6320            }
6321            return Ok(false);
6322        }
6323
6324        for child in node.children().iter() {
6325            if !child.visit(*self)? {
6326                return Ok(false);
6327            }
6328        }
6329
6330        Ok(true)
6331    }
6332}
6333
6334/// Locate the source `var` declaration corresponding to a sketch-var solution.
6335///
6336/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6337/// from "target found." The inner [`Option`] is the initial numeric literal of
6338/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6339///
6340/// When `node_path` is `None` (e.g. for older outcomes that predate the
6341/// node-path propagation), this falls back to source-range matching, which
6342/// can break under whitespace shifts elsewhere in the file.
6343fn numeric_literal_at_node_path(
6344    ast: &ast::Node<ast::Program>,
6345    node_path: Option<&ast::NodePath>,
6346    source_range: SourceRange,
6347) -> Option<Option<ast::NumericLiteral>> {
6348    let Some(node_path) = node_path else {
6349        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";
6350        #[cfg(target_arch = "wasm32")]
6351        web_sys::console::warn_1(&message.into());
6352        #[cfg(not(target_arch = "wasm32"))]
6353        eprintln!("WARNING: {message}");
6354        return numeric_literal_at_source_range(ast, source_range).map(Some);
6355    };
6356    let find = FindSketchVarInitialByNodePath {
6357        target: node_path,
6358        sketch_var_found: Cell::new(false),
6359        initial_literal: Cell::new(None),
6360    };
6361    let node = crate::walk::Node::from(ast);
6362    node.visit(&find).ok()?;
6363    if !find.sketch_var_found.get() {
6364        return None;
6365    }
6366    Some(find.initial_literal.into_inner())
6367}
6368
6369fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6370    match suffix {
6371        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6372        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6373        NumericSuffix::M => Some(UnitLength::Meters),
6374        NumericSuffix::Inch => Some(UnitLength::Inches),
6375        NumericSuffix::Ft => Some(UnitLength::Feet),
6376        NumericSuffix::Yd => Some(UnitLength::Yards),
6377        _ => None,
6378    }
6379}
6380
6381fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6382    match suffix_length_unit(number.units) {
6383        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6384        None => number.value,
6385    }
6386}
6387
6388fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6389    match suffix_length_unit(literal.suffix) {
6390        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6391        None => literal.value,
6392    }
6393}
6394
6395fn var_solution_needs_commit(
6396    current_literal: &ast::NumericLiteral,
6397    solved_value: Number,
6398    default_length_unit: UnitLength,
6399) -> bool {
6400    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6401    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6402
6403    (current - solved).abs() > 1e-9
6404}
6405
6406fn preserve_var_solution_literal_style(
6407    current_literal: &ast::NumericLiteral,
6408    solved_value: Number,
6409    default_length_unit: UnitLength,
6410) -> Number {
6411    if current_literal.suffix == NumericSuffix::None {
6412        return Number {
6413            value: number_value_in_default_length_units(solved_value, default_length_unit),
6414            units: NumericSuffix::None,
6415        };
6416    }
6417
6418    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6419        return solved_value;
6420    };
6421
6422    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6423    Number {
6424        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6425        units: current_literal.suffix,
6426    }
6427}
6428
6429pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6430    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6431        inner: ast::ArrayExpression {
6432            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6433            non_code_meta: Default::default(),
6434            digest: None,
6435        },
6436        start: Default::default(),
6437        end: Default::default(),
6438        module_id: Default::default(),
6439        node_path: None,
6440        outer_attrs: Default::default(),
6441        pre_comments: Default::default(),
6442        comment_start: Default::default(),
6443    })))
6444}
6445
6446pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6447    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6448        ast::ArrayExpression {
6449            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6450            digest: None,
6451            non_code_meta: Default::default(),
6452        },
6453    ))))
6454}
6455
6456fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6457    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6458        ast::ArrayExpression {
6459            elements: vec![
6460                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6461                    point.x,
6462                )?)))),
6463                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6464                    point.y,
6465                )?)))),
6466            ],
6467            non_code_meta: Default::default(),
6468            digest: None,
6469        },
6470    ))))
6471}
6472
6473fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6474    match expr {
6475        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6476            inner: ast::Literal::from(to_source_number(*number)?),
6477            start: Default::default(),
6478            end: Default::default(),
6479            module_id: Default::default(),
6480            node_path: None,
6481            outer_attrs: Default::default(),
6482            pre_comments: Default::default(),
6483            comment_start: Default::default(),
6484        }))),
6485        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6486            inner: ast::SketchVar {
6487                initial: Some(Box::new(ast::Node {
6488                    inner: to_source_number(*number)?,
6489                    start: Default::default(),
6490                    end: Default::default(),
6491                    module_id: Default::default(),
6492                    node_path: None,
6493                    outer_attrs: Default::default(),
6494                    pre_comments: Default::default(),
6495                    comment_start: Default::default(),
6496                })),
6497                digest: None,
6498            },
6499            start: Default::default(),
6500            end: Default::default(),
6501            module_id: Default::default(),
6502            node_path: None,
6503            outer_attrs: Default::default(),
6504            pre_comments: Default::default(),
6505            comment_start: Default::default(),
6506        }))),
6507        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6508    }
6509}
6510
6511fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6512    Ok(ast::NumericLiteral {
6513        value: number.value,
6514        suffix: number.units,
6515        raw: format_number_literal(number.value, number.units, None)?,
6516        digest: None,
6517    })
6518}
6519
6520pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6521    ast::Expr::Name(Box::new(ast_name(name)))
6522}
6523
6524fn ast_name(name: String) -> ast::Node<ast::Name> {
6525    ast::Node {
6526        inner: ast::Name {
6527            name: ast::Node {
6528                inner: ast::Identifier { name, digest: None },
6529                start: Default::default(),
6530                end: Default::default(),
6531                module_id: Default::default(),
6532                node_path: None,
6533                outer_attrs: Default::default(),
6534                pre_comments: Default::default(),
6535                comment_start: Default::default(),
6536            },
6537            path: Vec::new(),
6538            abs_path: false,
6539            digest: None,
6540        },
6541        start: Default::default(),
6542        end: Default::default(),
6543        module_id: Default::default(),
6544        node_path: None,
6545        outer_attrs: Default::default(),
6546        pre_comments: Default::default(),
6547        comment_start: Default::default(),
6548    }
6549}
6550
6551pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6552    ast::Name {
6553        name: ast::Node {
6554            inner: ast::Identifier {
6555                name: name.to_owned(),
6556                digest: None,
6557            },
6558            start: Default::default(),
6559            end: Default::default(),
6560            module_id: Default::default(),
6561            node_path: None,
6562            outer_attrs: Default::default(),
6563            pre_comments: Default::default(),
6564            comment_start: Default::default(),
6565        },
6566        path: Default::default(),
6567        abs_path: false,
6568        digest: None,
6569    }
6570}
6571
6572// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6573
6574/// Create an AST node for coincident([expr1, expr2, ...])
6575pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6576    let elements = exprs.into_iter().collect::<Vec<_>>();
6577    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6578
6579    // Create array [expr1, expr2, ...]
6580    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6581        elements,
6582        digest: None,
6583        non_code_meta: Default::default(),
6584    })));
6585
6586    // Create coincident([...])
6587    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6588        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6589        unlabeled: Some(array_expr),
6590        arguments: Default::default(),
6591        digest: None,
6592        non_code_meta: Default::default(),
6593    })))
6594}
6595
6596/// Create an AST node for line(start = [...], end = [...])
6597pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6598    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6599        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6600        unlabeled: None,
6601        arguments: vec![
6602            ast::LabeledArg {
6603                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6604                arg: start_ast,
6605            },
6606            ast::LabeledArg {
6607                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6608                arg: end_ast,
6609            },
6610        ],
6611        digest: None,
6612        non_code_meta: Default::default(),
6613    })))
6614}
6615
6616/// Create an AST node for arc(start = [...], end = [...], center = [...])
6617pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6618    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6619        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6620        unlabeled: None,
6621        arguments: vec![
6622            ast::LabeledArg {
6623                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6624                arg: start_ast,
6625            },
6626            ast::LabeledArg {
6627                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6628                arg: end_ast,
6629            },
6630            ast::LabeledArg {
6631                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6632                arg: center_ast,
6633            },
6634        ],
6635        digest: None,
6636        non_code_meta: Default::default(),
6637    })))
6638}
6639
6640/// Create an AST node for circle(start = [...], center = [...])
6641pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6642    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6643        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6644        unlabeled: None,
6645        arguments: vec![
6646            ast::LabeledArg {
6647                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6648                arg: start_ast,
6649            },
6650            ast::LabeledArg {
6651                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6652                arg: center_ast,
6653            },
6654        ],
6655        digest: None,
6656        non_code_meta: Default::default(),
6657    })))
6658}
6659
6660/// Create an AST node for horizontal(line)
6661pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6662    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6663        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6664        unlabeled: Some(line_expr),
6665        arguments: Default::default(),
6666        digest: None,
6667        non_code_meta: Default::default(),
6668    })))
6669}
6670
6671/// Create an AST node for vertical(line)
6672pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6673    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6674        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6675        unlabeled: Some(line_expr),
6676        arguments: Default::default(),
6677        digest: None,
6678        non_code_meta: Default::default(),
6679    })))
6680}
6681
6682/// Create a member expression like object.property (e.g., line1.end)
6683pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6684    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6685        object: object_expr,
6686        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6687            name: ast::Node::no_src(ast::Identifier {
6688                name: property.to_string(),
6689                digest: None,
6690            }),
6691            path: Vec::new(),
6692            abs_path: false,
6693            digest: None,
6694        }))),
6695        computed: false,
6696        digest: None,
6697    })))
6698}
6699
6700pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6701    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6702        object: object_expr,
6703        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6704            value: index as f64,
6705            suffix: NumericSuffix::None,
6706            raw: index.to_string(),
6707            digest: None,
6708        })))),
6709        computed: true,
6710        digest: None,
6711    })))
6712}
6713
6714/// Create an AST node for `fixed([point, [x, y]])`.
6715fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6716    // Create [x, y] array literal.
6717    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6718        position.x,
6719    )?))));
6720    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6721        position.y,
6722    )?))));
6723    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6724        elements: vec![x_literal, y_literal],
6725        digest: None,
6726        non_code_meta: Default::default(),
6727    })));
6728
6729    // Create [point, [x, y]] outer array.
6730    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6731        elements: vec![point_expr, point_array],
6732        digest: None,
6733        non_code_meta: Default::default(),
6734    })));
6735
6736    // Create fixed([...])
6737    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6738        ast::CallExpressionKw {
6739            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6740            unlabeled: Some(array_expr),
6741            arguments: Default::default(),
6742            digest: None,
6743            non_code_meta: Default::default(),
6744        },
6745    ))))
6746}
6747
6748/// Create an AST node for equalLength([line1, line2, ...])
6749pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6750    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6751        elements: line_exprs,
6752        digest: None,
6753        non_code_meta: Default::default(),
6754    })));
6755
6756    // Create equalLength([...])
6757    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6758        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6759        unlabeled: Some(array_expr),
6760        arguments: Default::default(),
6761        digest: None,
6762        non_code_meta: Default::default(),
6763    })))
6764}
6765
6766/// Create an AST node for equalRadius([seg1, seg2, ...])
6767pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6768    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6769        elements: segment_exprs,
6770        digest: None,
6771        non_code_meta: Default::default(),
6772    })));
6773
6774    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6775        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6776        unlabeled: Some(array_expr),
6777        arguments: Default::default(),
6778        digest: None,
6779        non_code_meta: Default::default(),
6780    })))
6781}
6782
6783/// Create an AST node for tangent([seg1, seg2])
6784pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6785    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6786        elements: vec![seg1_expr, seg2_expr],
6787        digest: None,
6788        non_code_meta: Default::default(),
6789    })));
6790
6791    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6792        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6793        unlabeled: Some(array_expr),
6794        arguments: Default::default(),
6795        digest: None,
6796        non_code_meta: Default::default(),
6797    })))
6798}
6799
6800/// Create an AST node for symmetric([input1, input2], axis = line)
6801pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6802    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6803        elements: input_exprs,
6804        digest: None,
6805        non_code_meta: Default::default(),
6806    })));
6807    let arguments = vec![ast::LabeledArg {
6808        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6809        arg: axis_expr,
6810    }];
6811
6812    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6813        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6814        unlabeled: Some(array_expr),
6815        arguments,
6816        digest: None,
6817        non_code_meta: Default::default(),
6818    })))
6819}
6820
6821/// Create an AST node for midpoint(segment, point = point)
6822pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6823    let arguments = vec![ast::LabeledArg {
6824        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6825        arg: point_expr,
6826    }];
6827
6828    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6829        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6830        unlabeled: Some(segment_expr),
6831        arguments,
6832        digest: None,
6833        non_code_meta: Default::default(),
6834    })))
6835}
6836
6837#[cfg(test)]
6838mod tests {
6839    use super::*;
6840    use crate::engine::PlaneName;
6841    use crate::execution::cache::SketchModeState;
6842    use crate::execution::cache::clear_mem_cache;
6843    use crate::execution::cache::read_old_memory;
6844    use crate::execution::cache::write_old_memory;
6845    use crate::front::Distance;
6846    use crate::front::Fixed;
6847    use crate::front::FixedPoint;
6848    use crate::front::Midpoint;
6849    use crate::front::Object;
6850    use crate::front::Plane;
6851    use crate::front::Sketch;
6852    use crate::front::Tangent;
6853    use crate::frontend::sketch::Vertical;
6854    use crate::pretty::NumericSuffix;
6855
6856    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6857        for object in &scene_graph.objects {
6858            if let ObjectKind::Sketch(_) = &object.kind {
6859                return Some(object);
6860            }
6861        }
6862        None
6863    }
6864
6865    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6866        for object in &scene_graph.objects {
6867            if let ObjectKind::Face(_) = &object.kind {
6868                return Some(object);
6869            }
6870        }
6871        None
6872    }
6873
6874    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6875        for object in &scene_graph.objects {
6876            if matches!(&object.kind, ObjectKind::Wall(_)) {
6877                return Some(object.id);
6878            }
6879        }
6880        None
6881    }
6882
6883    #[test]
6884    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6885        let source = "\
6886region001 = region(point = [0.1, 0.1], sketch = s)
6887extrude001 = extrude(region001, length = 5)
6888revolve001 = revolve(region001, axis = Y)
6889sweep001 = sweep(region001, path = path001)
6890loft001 = loft(region001)
6891not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6892";
6893
6894        let program = Program::parse(source).unwrap().0.unwrap();
6895
6896        assert_eq!(
6897            region_name_from_sweep_variable(&program.ast, "extrude001"),
6898            Some("region001".to_owned())
6899        );
6900        assert_eq!(
6901            region_name_from_sweep_variable(&program.ast, "revolve001"),
6902            Some("region001".to_owned())
6903        );
6904        assert_eq!(
6905            region_name_from_sweep_variable(&program.ast, "sweep001"),
6906            Some("region001".to_owned())
6907        );
6908        assert_eq!(
6909            region_name_from_sweep_variable(&program.ast, "loft001"),
6910            Some("region001".to_owned())
6911        );
6912        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6913    }
6914
6915    #[track_caller]
6916    fn expect_sketch(object: &Object) -> &Sketch {
6917        if let ObjectKind::Sketch(sketch) = &object.kind {
6918            sketch
6919        } else {
6920            panic!("Object is not a sketch: {:?}", object);
6921        }
6922    }
6923
6924    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6925        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6926        let ObjectKind::Segment {
6927            segment: Segment::Point(point),
6928        } = &point_object.kind
6929        else {
6930            panic!("Object is not a point segment: {point_object:?}");
6931        };
6932        point.position.clone()
6933    }
6934
6935    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6936        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6937        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6938    }
6939
6940    /// Build a millimeter-valued point expression for concise sketch edit test
6941    /// setup.
6942    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
6943        Point2d {
6944            x: Expr::Var(Number {
6945                value: x,
6946                units: NumericSuffix::Mm,
6947            }),
6948            y: Expr::Var(Number {
6949                value: y,
6950                units: NumericSuffix::Mm,
6951            }),
6952        }
6953    }
6954
6955    /// Build a millimeter-valued numeric point for comparing solved scene graph
6956    /// positions.
6957    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
6958        Point2d {
6959            x: Number {
6960                value: x,
6961                units: NumericSuffix::Mm,
6962            },
6963            y: Number {
6964                value: y,
6965                units: NumericSuffix::Mm,
6966            },
6967        }
6968    }
6969
6970    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6971        LineCtor {
6972            start: Point2d {
6973                x: Expr::Number(Number { value: start_x, units }),
6974                y: Expr::Number(Number { value: start_y, units }),
6975            },
6976            end: Point2d {
6977                x: Expr::Number(Number { value: end_x, units }),
6978                y: Expr::Number(Number { value: end_y, units }),
6979            },
6980            construction: None,
6981        }
6982    }
6983
6984    async fn create_sketch_with_single_line(
6985        frontend: &mut FrontendState,
6986        ctx: &ExecutorContext,
6987        mock_ctx: &ExecutorContext,
6988        version: Version,
6989    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6990        frontend.program = Program::empty();
6991
6992        let sketch_args = SketchCtor {
6993            on: Plane::Default(PlaneName::Xy),
6994        };
6995        let (_src_delta, _scene_delta, sketch_id) = frontend
6996            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6997            .await
6998            .unwrap();
6999
7000        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7001        let (source_delta, scene_graph_delta) = frontend
7002            .add_segment(mock_ctx, version, sketch_id, segment, None)
7003            .await
7004            .unwrap();
7005        let line_id = *scene_graph_delta
7006            .new_objects
7007            .last()
7008            .expect("Expected line object id to be created");
7009
7010        (sketch_id, line_id, source_delta, scene_graph_delta)
7011    }
7012
7013    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7014        frontend.program = program.clone();
7015        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7016        frontend.update_state_after_exec(outcome, true);
7017    }
7018
7019    #[tokio::test(flavor = "multi_thread")]
7020    async fn test_sketch_checkpoint_round_trip_restores_state() {
7021        let mut frontend = FrontendState::new();
7022        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7023        let mock_ctx = ExecutorContext::new_mock(None).await;
7024        let version = Version(0);
7025
7026        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7027            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7028
7029        let expected_source = source_delta.text.clone();
7030        let expected_scene_graph = frontend.scene_graph.clone();
7031        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7032        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7033
7034        let checkpoint_id = frontend
7035            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7036            .await
7037            .unwrap();
7038
7039        let edited_segments = vec![ExistingSegmentCtor {
7040            id: line_id,
7041            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7042        }];
7043        let (edited_source, _edited_scene) = frontend
7044            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7045            .await
7046            .unwrap();
7047        assert_ne!(edited_source.text, expected_source);
7048
7049        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7050
7051        assert_eq!(restored.source_delta.text, expected_source);
7052        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7053        assert!(restored.scene_graph_delta.invalidates_ids);
7054        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7055        assert_eq!(frontend.scene_graph, expected_scene_graph);
7056        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7057
7058        ctx.close().await;
7059    }
7060
7061    #[tokio::test(flavor = "multi_thread")]
7062    async fn test_sketch_checkpoints_prune_oldest_entries() {
7063        let mut frontend = FrontendState::new();
7064        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7065        let mock_ctx = ExecutorContext::new_mock(None).await;
7066        let version = Version(0);
7067
7068        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7069            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7070
7071        let mut checkpoint_ids = Vec::new();
7072        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7073            checkpoint_ids.push(
7074                frontend
7075                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7076                    .await
7077                    .unwrap(),
7078            );
7079        }
7080
7081        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7082        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7083
7084        let oldest_retained = checkpoint_ids[3];
7085        assert_eq!(
7086            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7087            Some(oldest_retained)
7088        );
7089
7090        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7091        assert!(evicted_restore.is_err());
7092        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7093
7094        frontend
7095            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7096            .await
7097            .unwrap();
7098
7099        ctx.close().await;
7100    }
7101
7102    #[tokio::test(flavor = "multi_thread")]
7103    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7104        let mut frontend = FrontendState::new();
7105        let missing_checkpoint = SketchCheckpointId::new(999);
7106
7107        let err = frontend
7108            .restore_sketch_checkpoint(missing_checkpoint)
7109            .await
7110            .expect_err("Expected restore to fail for missing checkpoint");
7111
7112        assert!(err.msg.contains("Sketch checkpoint not found"));
7113    }
7114
7115    #[tokio::test(flavor = "multi_thread")]
7116    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7117        let mut frontend = FrontendState::new();
7118        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7119        let mock_ctx = ExecutorContext::new_mock(None).await;
7120        let version = Version(0);
7121
7122        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7123            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7124
7125        let checkpoint_a = frontend
7126            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7127            .await
7128            .unwrap();
7129        let checkpoint_b = frontend
7130            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7131            .await
7132            .unwrap();
7133        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7134
7135        frontend.clear_sketch_checkpoints();
7136        assert!(frontend.sketch_checkpoints.is_empty());
7137        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7138        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7139
7140        ctx.close().await;
7141    }
7142
7143    #[tokio::test(flavor = "multi_thread")]
7144    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7145        let mut frontend = FrontendState::new();
7146        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7147        let mock_ctx = ExecutorContext::new_mock(None).await;
7148        let version = Version(0);
7149
7150        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7151            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7152        let old_source = source_delta.text.clone();
7153        let old_checkpoint = frontend
7154            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7155            .await
7156            .unwrap();
7157        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7158
7159        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7160            .unwrap()
7161            .0
7162            .unwrap();
7163
7164        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7165        let SetProgramOutcome::Success {
7166            checkpoint_id: Some(new_checkpoint),
7167            ..
7168        } = result
7169        else {
7170            panic!("Expected Success with a fresh checkpoint baseline");
7171        };
7172
7173        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7174
7175        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7176        assert_eq!(old_restore.source_delta.text, old_source);
7177
7178        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7179        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7180
7181        ctx.close().await;
7182    }
7183
7184    #[tokio::test(flavor = "multi_thread")]
7185    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7186        let mut frontend = FrontendState::new();
7187        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7188        let mock_ctx = ExecutorContext::new_mock(None).await;
7189        let version = Version(0);
7190
7191        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7192            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7193        let old_checkpoint = frontend
7194            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7195            .await
7196            .unwrap();
7197        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7198
7199        let failing_program = Program::parse(
7200            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7201        )
7202        .unwrap()
7203        .0
7204        .unwrap();
7205
7206        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7207        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7208        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7209        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7210
7211        ctx.close().await;
7212    }
7213
7214    #[tokio::test(flavor = "multi_thread")]
7215    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7216        let mut frontend = FrontendState::new();
7217        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7218
7219        let program = Program::parse(
7220            "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",
7221        )
7222        .unwrap()
7223        .0
7224        .unwrap();
7225        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7226        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7227            panic!("Expected successful baseline program execution");
7228        };
7229
7230        clear_mem_cache().await;
7231        assert!(read_old_memory().await.is_none());
7232
7233        let checkpoint_without_mock_memory = frontend
7234            .create_sketch_checkpoint((*exec_outcome).clone())
7235            .await
7236            .unwrap();
7237
7238        write_old_memory(SketchModeState::new_for_tests()).await;
7239        assert!(read_old_memory().await.is_some());
7240
7241        let checkpoint_with_mock_memory = frontend
7242            .create_sketch_checkpoint((*exec_outcome).clone())
7243            .await
7244            .unwrap();
7245
7246        clear_mem_cache().await;
7247        assert!(read_old_memory().await.is_none());
7248
7249        frontend
7250            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7251            .await
7252            .unwrap();
7253        assert!(read_old_memory().await.is_some());
7254
7255        frontend
7256            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7257            .await
7258            .unwrap();
7259        assert!(read_old_memory().await.is_none());
7260
7261        ctx.close().await;
7262    }
7263
7264    #[tokio::test(flavor = "multi_thread")]
7265    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7266        let source = "\
7267sketch(on = XY) {
7268  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7269}
7270
7271bad = missing_name
7272";
7273        let program = Program::parse(source).unwrap().0.unwrap();
7274
7275        let mut frontend = FrontendState::new();
7276
7277        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7278        let mock_ctx = ExecutorContext::new_mock(None).await;
7279        let version = Version(0);
7280        let project_id = ProjectId(0);
7281        let file_id = FileId(0);
7282
7283        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7284            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7285        };
7286
7287        let sketch_id = frontend
7288            .scene_graph
7289            .objects
7290            .iter()
7291            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7292            .expect("Expected sketch object from errored hack_set_program");
7293
7294        frontend
7295            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7296            .await
7297            .unwrap();
7298
7299        ctx.close().await;
7300        mock_ctx.close().await;
7301    }
7302
7303    #[tokio::test(flavor = "multi_thread")]
7304    async fn test_new_sketch_add_point_edit_point() {
7305        let program = Program::empty();
7306
7307        let mut frontend = FrontendState::new();
7308        frontend.program = program;
7309
7310        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7311        let mock_ctx = ExecutorContext::new_mock(None).await;
7312        let version = Version(0);
7313
7314        let sketch_args = SketchCtor {
7315            on: Plane::Default(PlaneName::Xy),
7316        };
7317        let (_src_delta, scene_delta, sketch_id) = frontend
7318            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7319            .await
7320            .unwrap();
7321        assert_eq!(sketch_id, ObjectId(1));
7322        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7323        let sketch_object = &scene_delta.new_graph.objects[1];
7324        assert_eq!(sketch_object.id, ObjectId(1));
7325        assert_eq!(
7326            sketch_object.kind,
7327            ObjectKind::Sketch(Sketch {
7328                args: SketchCtor {
7329                    on: Plane::Default(PlaneName::Xy)
7330                },
7331                plane: ObjectId(0),
7332                segments: vec![],
7333                constraints: vec![],
7334            })
7335        );
7336        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7337
7338        let point_ctor = PointCtor {
7339            position: Point2d {
7340                x: Expr::Number(Number {
7341                    value: 1.0,
7342                    units: NumericSuffix::Inch,
7343                }),
7344                y: Expr::Number(Number {
7345                    value: 2.0,
7346                    units: NumericSuffix::Inch,
7347                }),
7348            },
7349        };
7350        let segment = SegmentCtor::Point(point_ctor);
7351        let (src_delta, scene_delta) = frontend
7352            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7353            .await
7354            .unwrap();
7355        assert_eq!(
7356            src_delta.text.as_str(),
7357            "sketch001 = sketch(on = XY) {
7358  point(at = [1in, 2in])
7359}
7360"
7361        );
7362        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7363        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7364        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7365            assert_eq!(scene_object.id.0, i);
7366        }
7367
7368        let point_id = *scene_delta.new_objects.last().unwrap();
7369
7370        let point_ctor = PointCtor {
7371            position: Point2d {
7372                x: Expr::Number(Number {
7373                    value: 3.0,
7374                    units: NumericSuffix::Inch,
7375                }),
7376                y: Expr::Number(Number {
7377                    value: 4.0,
7378                    units: NumericSuffix::Inch,
7379                }),
7380            },
7381        };
7382        let segments = vec![ExistingSegmentCtor {
7383            id: point_id,
7384            ctor: SegmentCtor::Point(point_ctor),
7385        }];
7386        let (src_delta, scene_delta) = frontend
7387            .edit_segments(&mock_ctx, version, sketch_id, segments)
7388            .await
7389            .unwrap();
7390        assert_eq!(
7391            src_delta.text.as_str(),
7392            "sketch001 = sketch(on = XY) {
7393  point(at = [3in, 4in])
7394}
7395"
7396        );
7397        assert_eq!(scene_delta.new_objects, vec![]);
7398        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7399
7400        ctx.close().await;
7401        mock_ctx.close().await;
7402    }
7403
7404    #[tokio::test(flavor = "multi_thread")]
7405    async fn test_new_sketch_add_line_edit_line() {
7406        let program = Program::empty();
7407
7408        let mut frontend = FrontendState::new();
7409        frontend.program = program;
7410
7411        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7412        let mock_ctx = ExecutorContext::new_mock(None).await;
7413        let version = Version(0);
7414
7415        let sketch_args = SketchCtor {
7416            on: Plane::Default(PlaneName::Xy),
7417        };
7418        let (_src_delta, scene_delta, sketch_id) = frontend
7419            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7420            .await
7421            .unwrap();
7422        assert_eq!(sketch_id, ObjectId(1));
7423        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7424        let sketch_object = &scene_delta.new_graph.objects[1];
7425        assert_eq!(sketch_object.id, ObjectId(1));
7426        assert_eq!(
7427            sketch_object.kind,
7428            ObjectKind::Sketch(Sketch {
7429                args: SketchCtor {
7430                    on: Plane::Default(PlaneName::Xy)
7431                },
7432                plane: ObjectId(0),
7433                segments: vec![],
7434                constraints: vec![],
7435            })
7436        );
7437        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7438
7439        let line_ctor = LineCtor {
7440            start: Point2d {
7441                x: Expr::Number(Number {
7442                    value: 0.0,
7443                    units: NumericSuffix::Mm,
7444                }),
7445                y: Expr::Number(Number {
7446                    value: 0.0,
7447                    units: NumericSuffix::Mm,
7448                }),
7449            },
7450            end: Point2d {
7451                x: Expr::Number(Number {
7452                    value: 10.0,
7453                    units: NumericSuffix::Mm,
7454                }),
7455                y: Expr::Number(Number {
7456                    value: 10.0,
7457                    units: NumericSuffix::Mm,
7458                }),
7459            },
7460            construction: None,
7461        };
7462        let segment = SegmentCtor::Line(line_ctor);
7463        let (src_delta, scene_delta) = frontend
7464            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7465            .await
7466            .unwrap();
7467        assert_eq!(
7468            src_delta.text.as_str(),
7469            "sketch001 = sketch(on = XY) {
7470  line(start = [0mm, 0mm], end = [10mm, 10mm])
7471}
7472"
7473        );
7474        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7475        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7476        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7477            assert_eq!(scene_object.id.0, i);
7478        }
7479
7480        // The new objects are the end points and then the line.
7481        let line = *scene_delta.new_objects.last().unwrap();
7482
7483        let line_ctor = LineCtor {
7484            start: Point2d {
7485                x: Expr::Number(Number {
7486                    value: 1.0,
7487                    units: NumericSuffix::Mm,
7488                }),
7489                y: Expr::Number(Number {
7490                    value: 2.0,
7491                    units: NumericSuffix::Mm,
7492                }),
7493            },
7494            end: Point2d {
7495                x: Expr::Number(Number {
7496                    value: 13.0,
7497                    units: NumericSuffix::Mm,
7498                }),
7499                y: Expr::Number(Number {
7500                    value: 14.0,
7501                    units: NumericSuffix::Mm,
7502                }),
7503            },
7504            construction: None,
7505        };
7506        let segments = vec![ExistingSegmentCtor {
7507            id: line,
7508            ctor: SegmentCtor::Line(line_ctor),
7509        }];
7510        let (src_delta, scene_delta) = frontend
7511            .edit_segments(&mock_ctx, version, sketch_id, segments)
7512            .await
7513            .unwrap();
7514        assert_eq!(
7515            src_delta.text.as_str(),
7516            "sketch001 = sketch(on = XY) {
7517  line(start = [1mm, 2mm], end = [13mm, 14mm])
7518}
7519"
7520        );
7521        assert_eq!(scene_delta.new_objects, vec![]);
7522        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7523
7524        ctx.close().await;
7525        mock_ctx.close().await;
7526    }
7527
7528    #[tokio::test(flavor = "multi_thread")]
7529    async fn test_new_sketch_add_arc_edit_arc() {
7530        let program = Program::empty();
7531
7532        let mut frontend = FrontendState::new();
7533        frontend.program = program;
7534
7535        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7536        let mock_ctx = ExecutorContext::new_mock(None).await;
7537        let version = Version(0);
7538
7539        let sketch_args = SketchCtor {
7540            on: Plane::Default(PlaneName::Xy),
7541        };
7542        let (_src_delta, scene_delta, sketch_id) = frontend
7543            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7544            .await
7545            .unwrap();
7546        assert_eq!(sketch_id, ObjectId(1));
7547        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7548        let sketch_object = &scene_delta.new_graph.objects[1];
7549        assert_eq!(sketch_object.id, ObjectId(1));
7550        assert_eq!(
7551            sketch_object.kind,
7552            ObjectKind::Sketch(Sketch {
7553                args: SketchCtor {
7554                    on: Plane::Default(PlaneName::Xy),
7555                },
7556                plane: ObjectId(0),
7557                segments: vec![],
7558                constraints: vec![],
7559            })
7560        );
7561        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7562
7563        let arc_ctor = ArcCtor {
7564            start: Point2d {
7565                x: Expr::Var(Number {
7566                    value: 0.0,
7567                    units: NumericSuffix::Mm,
7568                }),
7569                y: Expr::Var(Number {
7570                    value: 0.0,
7571                    units: NumericSuffix::Mm,
7572                }),
7573            },
7574            end: Point2d {
7575                x: Expr::Var(Number {
7576                    value: 10.0,
7577                    units: NumericSuffix::Mm,
7578                }),
7579                y: Expr::Var(Number {
7580                    value: 10.0,
7581                    units: NumericSuffix::Mm,
7582                }),
7583            },
7584            center: Point2d {
7585                x: Expr::Var(Number {
7586                    value: 10.0,
7587                    units: NumericSuffix::Mm,
7588                }),
7589                y: Expr::Var(Number {
7590                    value: 0.0,
7591                    units: NumericSuffix::Mm,
7592                }),
7593            },
7594            construction: None,
7595        };
7596        let segment = SegmentCtor::Arc(arc_ctor);
7597        let (src_delta, scene_delta) = frontend
7598            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7599            .await
7600            .unwrap();
7601        assert_eq!(
7602            src_delta.text.as_str(),
7603            "sketch001 = sketch(on = XY) {
7604  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7605}
7606"
7607        );
7608        assert_eq!(
7609            scene_delta.new_objects,
7610            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7611        );
7612        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7613            assert_eq!(scene_object.id.0, i);
7614        }
7615        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7616
7617        // The new objects are the end points, the center, and then the arc.
7618        let arc = *scene_delta.new_objects.last().unwrap();
7619
7620        let arc_ctor = ArcCtor {
7621            start: Point2d {
7622                x: Expr::Var(Number {
7623                    value: 1.0,
7624                    units: NumericSuffix::Mm,
7625                }),
7626                y: Expr::Var(Number {
7627                    value: 2.0,
7628                    units: NumericSuffix::Mm,
7629                }),
7630            },
7631            end: Point2d {
7632                x: Expr::Var(Number {
7633                    value: 13.0,
7634                    units: NumericSuffix::Mm,
7635                }),
7636                y: Expr::Var(Number {
7637                    value: 14.0,
7638                    units: NumericSuffix::Mm,
7639                }),
7640            },
7641            center: Point2d {
7642                x: Expr::Var(Number {
7643                    value: 13.0,
7644                    units: NumericSuffix::Mm,
7645                }),
7646                y: Expr::Var(Number {
7647                    value: 2.0,
7648                    units: NumericSuffix::Mm,
7649                }),
7650            },
7651            construction: None,
7652        };
7653        let segments = vec![ExistingSegmentCtor {
7654            id: arc,
7655            ctor: SegmentCtor::Arc(arc_ctor),
7656        }];
7657        let (src_delta, scene_delta) = frontend
7658            .edit_segments(&mock_ctx, version, sketch_id, segments)
7659            .await
7660            .unwrap();
7661        assert_eq!(
7662            src_delta.text.as_str(),
7663            "sketch001 = sketch(on = XY) {
7664  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7665}
7666"
7667        );
7668        assert_eq!(scene_delta.new_objects, vec![]);
7669        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7670
7671        ctx.close().await;
7672        mock_ctx.close().await;
7673    }
7674
7675    #[tokio::test(flavor = "multi_thread")]
7676    async fn test_new_sketch_add_circle_edit_circle() {
7677        let program = Program::empty();
7678
7679        let mut frontend = FrontendState::new();
7680        frontend.program = program;
7681
7682        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7683        let mock_ctx = ExecutorContext::new_mock(None).await;
7684        let version = Version(0);
7685
7686        let sketch_args = SketchCtor {
7687            on: Plane::Default(PlaneName::Xy),
7688        };
7689        let (_src_delta, _scene_delta, sketch_id) = frontend
7690            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7691            .await
7692            .unwrap();
7693
7694        // Add a circle segment.
7695        let circle_ctor = CircleCtor {
7696            start: Point2d {
7697                x: Expr::Var(Number {
7698                    value: 5.0,
7699                    units: NumericSuffix::Mm,
7700                }),
7701                y: Expr::Var(Number {
7702                    value: 0.0,
7703                    units: NumericSuffix::Mm,
7704                }),
7705            },
7706            center: Point2d {
7707                x: Expr::Var(Number {
7708                    value: 0.0,
7709                    units: NumericSuffix::Mm,
7710                }),
7711                y: Expr::Var(Number {
7712                    value: 0.0,
7713                    units: NumericSuffix::Mm,
7714                }),
7715            },
7716            construction: None,
7717        };
7718        let segment = SegmentCtor::Circle(circle_ctor);
7719        let (src_delta, scene_delta) = frontend
7720            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7721            .await
7722            .unwrap();
7723        assert_eq!(
7724            src_delta.text.as_str(),
7725            "sketch001 = sketch(on = XY) {
7726  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7727}
7728"
7729        );
7730        // The new objects are start, center, and then the circle segment.
7731        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7732        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7733
7734        let circle = *scene_delta.new_objects.last().unwrap();
7735
7736        // Edit the circle segment.
7737        let circle_ctor = CircleCtor {
7738            start: Point2d {
7739                x: Expr::Var(Number {
7740                    value: 10.0,
7741                    units: NumericSuffix::Mm,
7742                }),
7743                y: Expr::Var(Number {
7744                    value: 0.0,
7745                    units: NumericSuffix::Mm,
7746                }),
7747            },
7748            center: Point2d {
7749                x: Expr::Var(Number {
7750                    value: 3.0,
7751                    units: NumericSuffix::Mm,
7752                }),
7753                y: Expr::Var(Number {
7754                    value: 4.0,
7755                    units: NumericSuffix::Mm,
7756                }),
7757            },
7758            construction: None,
7759        };
7760        let segments = vec![ExistingSegmentCtor {
7761            id: circle,
7762            ctor: SegmentCtor::Circle(circle_ctor),
7763        }];
7764        let (src_delta, scene_delta) = frontend
7765            .edit_segments(&mock_ctx, version, sketch_id, segments)
7766            .await
7767            .unwrap();
7768        assert_eq!(
7769            src_delta.text.as_str(),
7770            "sketch001 = sketch(on = XY) {
7771  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7772}
7773"
7774        );
7775        assert_eq!(scene_delta.new_objects, vec![]);
7776        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7777
7778        ctx.close().await;
7779        mock_ctx.close().await;
7780    }
7781
7782    #[tokio::test(flavor = "multi_thread")]
7783    async fn test_delete_circle() {
7784        let initial_source = "sketch001 = sketch(on = XY) {
7785  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7786}
7787";
7788
7789        let program = Program::parse(initial_source).unwrap().0.unwrap();
7790        let mut frontend = FrontendState::new();
7791
7792        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793        let mock_ctx = ExecutorContext::new_mock(None).await;
7794        let version = Version(0);
7795
7796        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7797        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7798        let sketch_id = sketch_object.id;
7799        let sketch = expect_sketch(sketch_object);
7800
7801        // The sketch should have 3 segments: start point, center point, and the circle.
7802        assert_eq!(sketch.segments.len(), 3);
7803        let circle_id = sketch.segments[2];
7804
7805        // Delete the circle.
7806        let (src_delta, scene_delta) = frontend
7807            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7808            .await
7809            .unwrap();
7810        assert_eq!(
7811            src_delta.text.as_str(),
7812            "sketch001 = sketch(on = XY) {
7813}
7814"
7815        );
7816        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7817        let new_sketch = expect_sketch(new_sketch_object);
7818        assert_eq!(new_sketch.segments.len(), 0);
7819
7820        ctx.close().await;
7821        mock_ctx.close().await;
7822    }
7823
7824    #[tokio::test(flavor = "multi_thread")]
7825    async fn test_edit_circle_via_point() {
7826        let initial_source = "sketch001 = sketch(on = XY) {
7827  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7828}
7829";
7830
7831        let program = Program::parse(initial_source).unwrap().0.unwrap();
7832        let mut frontend = FrontendState::new();
7833
7834        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7835        let mock_ctx = ExecutorContext::new_mock(None).await;
7836        let version = Version(0);
7837
7838        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7839        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7840        let sketch_id = sketch_object.id;
7841        let sketch = expect_sketch(sketch_object);
7842
7843        // Find the circle segment and its start point.
7844        let circle_id = sketch
7845            .segments
7846            .iter()
7847            .copied()
7848            .find(|seg_id| {
7849                matches!(
7850                    &frontend.scene_graph.objects[seg_id.0].kind,
7851                    ObjectKind::Segment {
7852                        segment: Segment::Circle(_)
7853                    }
7854                )
7855            })
7856            .expect("Expected a circle segment in sketch");
7857        let circle_object = &frontend.scene_graph.objects[circle_id.0];
7858        let ObjectKind::Segment {
7859            segment: Segment::Circle(circle),
7860        } = &circle_object.kind
7861        else {
7862            panic!("Expected circle segment, got: {:?}", circle_object.kind);
7863        };
7864        let start_point_id = circle.start;
7865
7866        // Edit the start point via SegmentCtor::Point.
7867        let segments = vec![ExistingSegmentCtor {
7868            id: start_point_id,
7869            ctor: SegmentCtor::Point(PointCtor {
7870                position: Point2d {
7871                    x: Expr::Var(Number {
7872                        value: 7.0,
7873                        units: NumericSuffix::Mm,
7874                    }),
7875                    y: Expr::Var(Number {
7876                        value: 1.0,
7877                        units: NumericSuffix::Mm,
7878                    }),
7879                },
7880            }),
7881        }];
7882        let (src_delta, _scene_delta) = frontend
7883            .edit_segments(&mock_ctx, version, sketch_id, segments)
7884            .await
7885            .unwrap();
7886        assert_eq!(
7887            src_delta.text.as_str(),
7888            "sketch001 = sketch(on = XY) {
7889  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7890}
7891"
7892        );
7893
7894        ctx.close().await;
7895        mock_ctx.close().await;
7896    }
7897
7898    #[tokio::test(flavor = "multi_thread")]
7899    async fn test_add_line_when_sketch_block_uses_variable() {
7900        let initial_source = "s = sketch(on = XY) {}
7901";
7902
7903        let program = Program::parse(initial_source).unwrap().0.unwrap();
7904
7905        let mut frontend = FrontendState::new();
7906
7907        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7908        let mock_ctx = ExecutorContext::new_mock(None).await;
7909        let version = Version(0);
7910
7911        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7912        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7913        let sketch_id = sketch_object.id;
7914
7915        let line_ctor = LineCtor {
7916            start: Point2d {
7917                x: Expr::Number(Number {
7918                    value: 0.0,
7919                    units: NumericSuffix::Mm,
7920                }),
7921                y: Expr::Number(Number {
7922                    value: 0.0,
7923                    units: NumericSuffix::Mm,
7924                }),
7925            },
7926            end: Point2d {
7927                x: Expr::Number(Number {
7928                    value: 10.0,
7929                    units: NumericSuffix::Mm,
7930                }),
7931                y: Expr::Number(Number {
7932                    value: 10.0,
7933                    units: NumericSuffix::Mm,
7934                }),
7935            },
7936            construction: None,
7937        };
7938        let segment = SegmentCtor::Line(line_ctor);
7939        let (src_delta, scene_delta) = frontend
7940            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7941            .await
7942            .unwrap();
7943        assert_eq!(
7944            src_delta.text.as_str(),
7945            "s = sketch(on = XY) {
7946  line(start = [0mm, 0mm], end = [10mm, 10mm])
7947}
7948"
7949        );
7950        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7951        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7952
7953        ctx.close().await;
7954        mock_ctx.close().await;
7955    }
7956
7957    #[tokio::test(flavor = "multi_thread")]
7958    async fn test_new_sketch_add_line_delete_sketch() {
7959        let program = Program::empty();
7960
7961        let mut frontend = FrontendState::new();
7962        frontend.program = program;
7963
7964        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7965        let mock_ctx = ExecutorContext::new_mock(None).await;
7966        let version = Version(0);
7967
7968        let sketch_args = SketchCtor {
7969            on: Plane::Default(PlaneName::Xy),
7970        };
7971        let (_src_delta, scene_delta, sketch_id) = frontend
7972            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7973            .await
7974            .unwrap();
7975        assert_eq!(sketch_id, ObjectId(1));
7976        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7977        let sketch_object = &scene_delta.new_graph.objects[1];
7978        assert_eq!(sketch_object.id, ObjectId(1));
7979        assert_eq!(
7980            sketch_object.kind,
7981            ObjectKind::Sketch(Sketch {
7982                args: SketchCtor {
7983                    on: Plane::Default(PlaneName::Xy)
7984                },
7985                plane: ObjectId(0),
7986                segments: vec![],
7987                constraints: vec![],
7988            })
7989        );
7990        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7991
7992        let line_ctor = LineCtor {
7993            start: Point2d {
7994                x: Expr::Number(Number {
7995                    value: 0.0,
7996                    units: NumericSuffix::Mm,
7997                }),
7998                y: Expr::Number(Number {
7999                    value: 0.0,
8000                    units: NumericSuffix::Mm,
8001                }),
8002            },
8003            end: Point2d {
8004                x: Expr::Number(Number {
8005                    value: 10.0,
8006                    units: NumericSuffix::Mm,
8007                }),
8008                y: Expr::Number(Number {
8009                    value: 10.0,
8010                    units: NumericSuffix::Mm,
8011                }),
8012            },
8013            construction: None,
8014        };
8015        let segment = SegmentCtor::Line(line_ctor);
8016        let (src_delta, scene_delta) = frontend
8017            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8018            .await
8019            .unwrap();
8020        assert_eq!(
8021            src_delta.text.as_str(),
8022            "sketch001 = sketch(on = XY) {
8023  line(start = [0mm, 0mm], end = [10mm, 10mm])
8024}
8025"
8026        );
8027        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8028
8029        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8030        assert_eq!(src_delta.text.as_str(), "");
8031        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8032
8033        ctx.close().await;
8034        mock_ctx.close().await;
8035    }
8036
8037    #[tokio::test(flavor = "multi_thread")]
8038    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8039        let initial_source = "s = sketch(on = XY) {}
8040";
8041
8042        let program = Program::parse(initial_source).unwrap().0.unwrap();
8043
8044        let mut frontend = FrontendState::new();
8045
8046        let ctx = ExecutorContext::new_with_engine(
8047            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8048            Default::default(),
8049        );
8050        let version = Version(0);
8051
8052        frontend.hack_set_program(&ctx, program).await.unwrap();
8053        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8054        let sketch_id = sketch_object.id;
8055
8056        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8057        assert_eq!(src_delta.text.as_str(), "");
8058        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8059
8060        ctx.close().await;
8061    }
8062
8063    #[tokio::test(flavor = "multi_thread")]
8064    async fn test_delete_sketch_after_comment() {
8065        let initial_source = "sketch001 = sketch(on = XZ) {
8066}
8067";
8068
8069        let program = Program::parse(initial_source).unwrap().0.unwrap();
8070        let mut frontend = FrontendState::new();
8071
8072        let ctx = ExecutorContext::new_with_engine(
8073            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8074            Default::default(),
8075        );
8076        let version = Version(0);
8077
8078        frontend.hack_set_program(&ctx, program).await.unwrap();
8079        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8080        let sketch_id = sketch_object.id;
8081        let original_source = sketch_object.source.clone();
8082
8083        let commented_source = "// test 1
8084sketch001 = sketch(on = XZ) {
8085}
8086";
8087        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8088        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8089
8090        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8091        assert_eq!(cached_sketch_object.source, original_source);
8092
8093        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8094        assert!(
8095            !src_delta.text.contains("sketch001"),
8096            "sketch was not deleted: {}",
8097            src_delta.text
8098        );
8099        // The leading line comment must survive deletion.
8100        assert_eq!(src_delta.text.as_str(), "// test 1\n");
8101        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8102
8103        ctx.close().await;
8104    }
8105
8106    #[tokio::test(flavor = "multi_thread")]
8107    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8108        let initial_source = "sketch001 = sketch(on = XZ) {
8109}
8110foo = 1
8111";
8112
8113        let program = Program::parse(initial_source).unwrap().0.unwrap();
8114        let mut frontend = FrontendState::new();
8115
8116        let ctx = ExecutorContext::new_with_engine(
8117            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8118            Default::default(),
8119        );
8120        let version = Version(0);
8121
8122        frontend.hack_set_program(&ctx, program).await.unwrap();
8123        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8124        let sketch_id = sketch_object.id;
8125
8126        let commented_source = "// keep me
8127sketch001 = sketch(on = XZ) {
8128}
8129foo = 1
8130";
8131        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8132        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8133
8134        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8135        // The leading comment should remain, now attached to the following body item.
8136        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8137
8138        ctx.close().await;
8139    }
8140
8141    #[tokio::test(flavor = "multi_thread")]
8142    async fn test_delete_segment_preserves_pre_comment() {
8143        let initial_source = "\
8144sketch(on = XY) {
8145  point(at = [var 1, var 2])
8146  // describe the middle point
8147  point(at = [var 3, var 4])
8148  point(at = [var 5, var 6])
8149}
8150";
8151
8152        let program = Program::parse(initial_source).unwrap().0.unwrap();
8153        let mut frontend = FrontendState::new();
8154
8155        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8156        let mock_ctx = ExecutorContext::new_mock(None).await;
8157        let version = Version(0);
8158
8159        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8160        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8161        let sketch_id = sketch_object.id;
8162        let sketch = expect_sketch(sketch_object);
8163
8164        let middle_point_id = *sketch.segments.get(1).unwrap();
8165
8166        let (src_delta, _scene_delta) = frontend
8167            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8168            .await
8169            .unwrap();
8170        // The line comment on the line above the deleted point must be preserved.
8171        // It is reattached to the next surviving body item.
8172        assert_eq!(
8173            src_delta.text.as_str(),
8174            "\
8175sketch(on = XY) {
8176  point(at = [var 1, var 2])
8177  // describe the middle point
8178  point(at = [var 5, var 6])
8179}
8180"
8181        );
8182
8183        ctx.close().await;
8184        mock_ctx.close().await;
8185    }
8186
8187    #[tokio::test(flavor = "multi_thread")]
8188    async fn test_delete_last_segment_preserves_pre_comment() {
8189        let initial_source = "\
8190sketch(on = XY) {
8191  point(at = [var 1, var 2])
8192  // describe the trailing point
8193  point(at = [var 3, var 4])
8194}
8195";
8196
8197        let program = Program::parse(initial_source).unwrap().0.unwrap();
8198        let mut frontend = FrontendState::new();
8199
8200        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8201        let mock_ctx = ExecutorContext::new_mock(None).await;
8202        let version = Version(0);
8203
8204        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8205        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8206        let sketch_id = sketch_object.id;
8207        let sketch = expect_sketch(sketch_object);
8208
8209        let last_point_id = *sketch.segments.last().unwrap();
8210
8211        let (src_delta, _scene_delta) = frontend
8212            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8213            .await
8214            .unwrap();
8215        // No following item to attach to; the comment is kept inside the sketch
8216        // block as trailing non-code metadata so the user does not lose it.
8217        assert_eq!(
8218            src_delta.text.as_str(),
8219            "\
8220sketch(on = XY) {
8221  point(at = [var 1, var 2])
8222  // describe the trailing point
8223}
8224"
8225        );
8226
8227        ctx.close().await;
8228        mock_ctx.close().await;
8229    }
8230
8231    #[tokio::test(flavor = "multi_thread")]
8232    async fn test_delete_segment_drops_inline_trailing_comment() {
8233        let initial_source = "\
8234sketch(on = XY) {
8235  point(at = [var 1, var 2])
8236  point(at = [var 3, var 4]) // same-line note that gets dropped
8237  point(at = [var 5, var 6])
8238}
8239";
8240
8241        let program = Program::parse(initial_source).unwrap().0.unwrap();
8242        let mut frontend = FrontendState::new();
8243
8244        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8245        let mock_ctx = ExecutorContext::new_mock(None).await;
8246        let version = Version(0);
8247
8248        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8249        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8250        let sketch_id = sketch_object.id;
8251        let sketch = expect_sketch(sketch_object);
8252
8253        let middle_point_id = *sketch.segments.get(1).unwrap();
8254
8255        let (src_delta, _scene_delta) = frontend
8256            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8257            .await
8258            .unwrap();
8259        // The same-line trailing comment is removed along with the deleted code.
8260        assert!(
8261            !src_delta.text.contains("same-line note"),
8262            "inline comment should have been removed: {}",
8263            src_delta.text
8264        );
8265
8266        ctx.close().await;
8267        mock_ctx.close().await;
8268    }
8269
8270    #[tokio::test(flavor = "multi_thread")]
8271    async fn test_delete_segments_preserves_block_comments_across_positions() {
8272        // One test exercising several `delete_body_item_preserving_pre_comments`
8273        // branches at once with `/* ... */` block comments:
8274        //   - first point: leading block comment must migrate to the next item.
8275        //   - first point: same-line trailing block comment must be dropped.
8276        //   - middle point: leading block comment must stay attached after migration.
8277        //   - last point: leading block comment, with no surviving next item,
8278        //     must be converted into a trailing NonCodeNode.
8279        let initial_source = "\
8280sketch(on = XY) {
8281  /* above first - moves to middle */
8282  point(at = [var 1, var 2]) /* same-line on first - dropped */
8283  /* above middle - stays */
8284  point(at = [var 3, var 4])
8285  /* above last - moves to trailing meta */
8286  point(at = [var 5, var 6])
8287}
8288";
8289
8290        let program = Program::parse(initial_source).unwrap().0.unwrap();
8291        let mut frontend = FrontendState::new();
8292
8293        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8294        let mock_ctx = ExecutorContext::new_mock(None).await;
8295        let version = Version(0);
8296
8297        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8298        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8299        let sketch_id = sketch_object.id;
8300        let sketch = expect_sketch(sketch_object);
8301
8302        let first_point_id = *sketch.segments.first().unwrap();
8303        let last_point_id = *sketch.segments.last().unwrap();
8304
8305        let (src_delta, _scene_delta) = frontend
8306            .delete_objects(
8307                &mock_ctx,
8308                version,
8309                sketch_id,
8310                Vec::new(),
8311                vec![first_point_id, last_point_id],
8312            )
8313            .await
8314            .unwrap();
8315        assert_eq!(
8316            src_delta.text.as_str(),
8317            "\
8318sketch(on = XY) {
8319  /* above first - moves to middle */
8320  /* above middle - stays */
8321  point(at = [var 3, var 4])
8322  /* above last - moves to trailing meta */
8323}
8324"
8325        );
8326
8327        ctx.close().await;
8328        mock_ctx.close().await;
8329    }
8330
8331    #[tokio::test(flavor = "multi_thread")]
8332    async fn test_edit_line_when_editing_its_start_point() {
8333        let initial_source = "\
8334sketch(on = XY) {
8335  line(start = [var 1, var 2], end = [var 3, var 4])
8336}
8337";
8338
8339        let program = Program::parse(initial_source).unwrap().0.unwrap();
8340
8341        let mut frontend = FrontendState::new();
8342
8343        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8344        let mock_ctx = ExecutorContext::new_mock(None).await;
8345        let version = Version(0);
8346
8347        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8348        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8349        let sketch_id = sketch_object.id;
8350        let sketch = expect_sketch(sketch_object);
8351
8352        let point_id = *sketch.segments.first().unwrap();
8353
8354        let point_ctor = PointCtor {
8355            position: Point2d {
8356                x: Expr::Var(Number {
8357                    value: 5.0,
8358                    units: NumericSuffix::Inch,
8359                }),
8360                y: Expr::Var(Number {
8361                    value: 6.0,
8362                    units: NumericSuffix::Inch,
8363                }),
8364            },
8365        };
8366        let segments = vec![ExistingSegmentCtor {
8367            id: point_id,
8368            ctor: SegmentCtor::Point(point_ctor),
8369        }];
8370        let (src_delta, scene_delta) = frontend
8371            .edit_segments(&mock_ctx, version, sketch_id, segments)
8372            .await
8373            .unwrap();
8374        assert_eq!(
8375            src_delta.text.as_str(),
8376            "\
8377sketch(on = XY) {
8378  line(start = [var 5in, var 6in], end = [var 3, var 4])
8379}
8380"
8381        );
8382        assert_eq!(scene_delta.new_objects, vec![]);
8383        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8384
8385        ctx.close().await;
8386        mock_ctx.close().await;
8387    }
8388
8389    #[tokio::test(flavor = "multi_thread")]
8390    async fn test_edit_line_when_editing_its_end_point() {
8391        let initial_source = "\
8392sketch(on = XY) {
8393  line(start = [var 1, var 2], end = [var 3, var 4])
8394}
8395";
8396
8397        let program = Program::parse(initial_source).unwrap().0.unwrap();
8398
8399        let mut frontend = FrontendState::new();
8400
8401        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8402        let mock_ctx = ExecutorContext::new_mock(None).await;
8403        let version = Version(0);
8404
8405        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8406        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8407        let sketch_id = sketch_object.id;
8408        let sketch = expect_sketch(sketch_object);
8409        let point_id = *sketch.segments.get(1).unwrap();
8410
8411        let point_ctor = PointCtor {
8412            position: Point2d {
8413                x: Expr::Var(Number {
8414                    value: 5.0,
8415                    units: NumericSuffix::Inch,
8416                }),
8417                y: Expr::Var(Number {
8418                    value: 6.0,
8419                    units: NumericSuffix::Inch,
8420                }),
8421            },
8422        };
8423        let segments = vec![ExistingSegmentCtor {
8424            id: point_id,
8425            ctor: SegmentCtor::Point(point_ctor),
8426        }];
8427        let (src_delta, scene_delta) = frontend
8428            .edit_segments(&mock_ctx, version, sketch_id, segments)
8429            .await
8430            .unwrap();
8431        assert_eq!(
8432            src_delta.text.as_str(),
8433            "\
8434sketch(on = XY) {
8435  line(start = [var 1, var 2], end = [var 5in, var 6in])
8436}
8437"
8438        );
8439        assert_eq!(scene_delta.new_objects, vec![]);
8440        assert_eq!(
8441            scene_delta.new_graph.objects.len(),
8442            5,
8443            "{:#?}",
8444            scene_delta.new_graph.objects
8445        );
8446
8447        ctx.close().await;
8448        mock_ctx.close().await;
8449    }
8450
8451    #[tokio::test(flavor = "multi_thread")]
8452    async fn test_edit_line_with_coincident_feedback() {
8453        let initial_source = "\
8454sketch(on = XY) {
8455  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8456  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8457  fixed([line1.start, [0, 0]])
8458  coincident([line1.end, line2.start])
8459  equalLength([line1, line2])
8460}
8461";
8462
8463        let program = Program::parse(initial_source).unwrap().0.unwrap();
8464
8465        let mut frontend = FrontendState::new();
8466
8467        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8468        let mock_ctx = ExecutorContext::new_mock(None).await;
8469        let version = Version(0);
8470
8471        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8472        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8473        let sketch_id = sketch_object.id;
8474        let sketch = expect_sketch(sketch_object);
8475        let line2_end_id = *sketch.segments.get(4).unwrap();
8476
8477        let segments = vec![ExistingSegmentCtor {
8478            id: line2_end_id,
8479            ctor: SegmentCtor::Point(PointCtor {
8480                position: Point2d {
8481                    x: Expr::Var(Number {
8482                        value: 9.0,
8483                        units: NumericSuffix::None,
8484                    }),
8485                    y: Expr::Var(Number {
8486                        value: 10.0,
8487                        units: NumericSuffix::None,
8488                    }),
8489                },
8490            }),
8491        }];
8492        let (src_delta, scene_delta) = frontend
8493            .edit_segments(&mock_ctx, version, sketch_id, segments)
8494            .await
8495            .unwrap();
8496        assert_eq!(
8497            src_delta.text.as_str(),
8498            "\
8499sketch(on = XY) {
8500  line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8501  line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8502  fixed([line1.start, [0, 0]])
8503  coincident([line1.end, line2.start])
8504  equalLength([line1, line2])
8505}
8506"
8507        );
8508        assert_eq!(
8509            scene_delta.new_graph.objects.len(),
8510            11,
8511            "{:#?}",
8512            scene_delta.new_graph.objects
8513        );
8514
8515        ctx.close().await;
8516        mock_ctx.close().await;
8517    }
8518
8519    #[tokio::test(flavor = "multi_thread")]
8520    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8521        let initial_source = "\
8522sketch(on = XY) {
8523  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8524  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8525  fixed([line1.start, [0, 0]])
8526  coincident([line1.end, line2.start])
8527  equalLength([line1, line2])
8528}
8529";
8530
8531        let program = Program::parse(initial_source).unwrap().0.unwrap();
8532        let mut frontend = FrontendState::new();
8533        let mock_ctx = ExecutorContext::new_mock(None).await;
8534        let version = Version(0);
8535
8536        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8537        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8538        let sketch_id = sketch_object.id;
8539        let sketch = expect_sketch(sketch_object);
8540        let line2_end_id = *sketch.segments.get(4).unwrap();
8541
8542        let segments = vec![ExistingSegmentCtor {
8543            id: line2_end_id,
8544            ctor: SegmentCtor::Point(PointCtor {
8545                position: Point2d {
8546                    x: Expr::Var(Number {
8547                        value: 9.0,
8548                        units: NumericSuffix::None,
8549                    }),
8550                    y: Expr::Var(Number {
8551                        value: 10.0,
8552                        units: NumericSuffix::None,
8553                    }),
8554                },
8555            }),
8556        }];
8557        let (edited_source, _) = frontend
8558            .edit_segments(&mock_ctx, version, sketch_id, segments)
8559            .await
8560            .unwrap();
8561
8562        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8563        assert_eq!(mock_source.text, edited_source.text);
8564
8565        mock_ctx.close().await;
8566    }
8567
8568    /// Preview segment edits should return solved geometry without persisting
8569    /// solver feedback to KCL.
8570    #[tokio::test(flavor = "multi_thread")]
8571    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8572        let initial_source = "\
8573sketch(on = XY) {
8574  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8575  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8576  fixed([line1.start, [0, 0]])
8577  coincident([line1.end, line2.start])
8578  equalLength([line1, line2])
8579}
8580";
8581
8582        let program = Program::parse(initial_source).unwrap().0.unwrap();
8583        let mut frontend = FrontendState::new();
8584        let mock_ctx = ExecutorContext::new_mock(None).await;
8585        let version = Version(0);
8586
8587        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8588        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8589        let sketch_id = sketch_object.id;
8590        let sketch = expect_sketch(sketch_object);
8591        let line2_end_id = *sketch.segments.get(4).unwrap();
8592
8593        let segments = vec![ExistingSegmentCtor {
8594            id: line2_end_id,
8595            ctor: SegmentCtor::Point(PointCtor {
8596                position: Point2d {
8597                    x: Expr::Var(Number {
8598                        value: 9.0,
8599                        units: NumericSuffix::None,
8600                    }),
8601                    y: Expr::Var(Number {
8602                        value: 10.0,
8603                        units: NumericSuffix::None,
8604                    }),
8605                },
8606            }),
8607        }];
8608        let (preview_source, preview_delta) = frontend
8609            .edit_segments_with_options(
8610                &mock_ctx,
8611                version,
8612                sketch_id,
8613                segments,
8614                EditSegmentsOptions {
8615                    anchor_segment_ids: Some(vec![line2_end_id]),
8616                    drag_anchors: Vec::new(),
8617                    commit_solved_initial_guesses: false,
8618                },
8619            )
8620            .await
8621            .unwrap();
8622
8623        assert!(
8624            !preview_delta.exec_outcome.var_solutions.is_empty(),
8625            "preview solve should still solve and return geometry feedback"
8626        );
8627        assert!(
8628            preview_source
8629                .text
8630                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8631        );
8632        assert!(
8633            preview_source
8634                .text
8635                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8636        );
8637
8638        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8639        assert_eq!(mock_source.text, preview_source.text);
8640
8641        mock_ctx.close().await;
8642    }
8643
8644    #[tokio::test(flavor = "multi_thread")]
8645    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8646        let initial_source = "\
8647sketch(on = XY) {
8648  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8649}
8650";
8651
8652        let program = Program::parse(initial_source).unwrap().0.unwrap();
8653        let mut frontend = FrontendState::new();
8654        let mock_ctx = ExecutorContext::new_mock(None).await;
8655        let version = Version(0);
8656
8657        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8658        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8659        let sketch_id = sketch_object.id;
8660        let sketch = expect_sketch(sketch_object);
8661        let line_end_id = *sketch.segments.get(1).unwrap();
8662
8663        let constraint = Constraint::Fixed(Fixed {
8664            points: vec![FixedPoint {
8665                point: line_end_id,
8666                position: Point2d {
8667                    x: Number {
8668                        value: 20.0,
8669                        units: NumericSuffix::Mm,
8670                    },
8671                    y: Number {
8672                        value: 0.0,
8673                        units: NumericSuffix::Mm,
8674                    },
8675                },
8676            }],
8677        });
8678        let (constraint_source, _) = frontend
8679            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8680            .await
8681            .unwrap();
8682
8683        assert!(
8684            constraint_source
8685                .text
8686                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8687            "{}",
8688            constraint_source.text
8689        );
8690        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8691        assert_eq!(mock_source.text, constraint_source.text);
8692
8693        mock_ctx.close().await;
8694    }
8695
8696    #[test]
8697    fn test_no_solver_feedback_preserves_original_source() {
8698        let initial_source = "\
8699@settings(defaultLengthUnit = in, kclVersion = 2.0)
8700cylinder = startSketchOn(XY)
8701    |> circle(center= [0, 0], radius= 22)
8702    |> extrude(length = 14)
8703";
8704        let mut frontend = FrontendState::new();
8705        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8706        let outcome = ExecOutcome {
8707            variables: Default::default(),
8708            operations: Default::default(),
8709            artifact_graph: Default::default(),
8710            scene_objects: Default::default(),
8711            source_range_to_object: Default::default(),
8712            var_solutions: Default::default(),
8713            issues: Default::default(),
8714            filenames: Default::default(),
8715            default_planes: Default::default(),
8716        };
8717
8718        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8719
8720        assert_eq!(source_delta.text, initial_source);
8721    }
8722
8723    /// Explicit drag anchors should limit which edited points become temporary
8724    /// fixed constraints.
8725    #[tokio::test(flavor = "multi_thread")]
8726    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8727        let initial_source = "\
8728sketch(on = XY) {
8729  point1 = point(at = [var 0mm, var 0mm])
8730  point2 = point(at = [var 0mm, var 0mm])
8731  coincident([point1, point2])
8732}
8733";
8734
8735        let program = Program::parse(initial_source).unwrap().0.unwrap();
8736        let mut frontend = FrontendState::new();
8737        let mock_ctx = ExecutorContext::new_mock(None).await;
8738        let version = Version(0);
8739
8740        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8741        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8742        let sketch_id = sketch_object.id;
8743        let sketch = expect_sketch(sketch_object);
8744        let point1_id = sketch.segments[0];
8745        let point2_id = sketch.segments[1];
8746
8747        let segments = vec![
8748            ExistingSegmentCtor {
8749                id: point1_id,
8750                ctor: SegmentCtor::Point(PointCtor {
8751                    position: point_expr_mm(10.0, 0.0),
8752                }),
8753            },
8754            ExistingSegmentCtor {
8755                id: point2_id,
8756                ctor: SegmentCtor::Point(PointCtor {
8757                    position: point_expr_mm(100.0, 0.0),
8758                }),
8759            },
8760        ];
8761        let (_, scene_delta) = frontend
8762            .edit_segments_with_options(
8763                &mock_ctx,
8764                version,
8765                sketch_id,
8766                segments,
8767                EditSegmentsOptions {
8768                    anchor_segment_ids: Some(vec![point1_id]),
8769                    drag_anchors: Vec::new(),
8770                    commit_solved_initial_guesses: true,
8771                },
8772            )
8773            .await
8774            .unwrap();
8775
8776        assert_point_position_close(
8777            point_position(&scene_delta.new_graph, point1_id),
8778            point_number_mm(10.0, 0.0),
8779        );
8780        assert_point_position_close(
8781            point_position(&scene_delta.new_graph, point2_id),
8782            point_number_mm(10.0, 0.0),
8783        );
8784
8785        mock_ctx.close().await;
8786    }
8787
8788    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
8789    /// for every SketchVar whose initial NumericLiteral has the given value.
8790    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8791        use std::cell::RefCell;
8792        struct Collector {
8793            target: f64,
8794            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8795        }
8796        impl<'a> crate::walk::Visitor<'a> for &Collector {
8797            type Error = crate::front::Error;
8798            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8799                if let crate::walk::Node::SketchVar(sketch_var) = node
8800                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8801                    && (initial.value - self.target).abs() < 1e-9
8802                {
8803                    self.out
8804                        .borrow_mut()
8805                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8806                }
8807                for child in node.children().iter() {
8808                    if !child.visit(*self)? {
8809                        return Ok(false);
8810                    }
8811                }
8812                Ok(true)
8813            }
8814        }
8815        let collector = Collector {
8816            target: value,
8817            out: Default::default(),
8818        };
8819        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8820        collector.out.into_inner()
8821    }
8822
8823    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
8824    /// for every SketchVar (including bare `var`).
8825    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8826        use std::cell::RefCell;
8827        struct Collector {
8828            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8829        }
8830        impl<'a> crate::walk::Visitor<'a> for &Collector {
8831            type Error = crate::front::Error;
8832            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8833                if let crate::walk::Node::SketchVar(sketch_var) = node
8834                    && let Some(node_path) = &sketch_var.node_path
8835                {
8836                    self.out
8837                        .borrow_mut()
8838                        .push((SourceRange::from(sketch_var), node_path.clone()));
8839                }
8840                for child in node.children().iter() {
8841                    if !child.visit(*self)? {
8842                        return Ok(false);
8843                    }
8844                }
8845                Ok(true)
8846            }
8847        }
8848        let collector = Collector {
8849            out: Default::default(),
8850        };
8851        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8852        collector.out.into_inner()
8853    }
8854
8855    fn empty_exec_outcome_with_var_solutions(
8856        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8857    ) -> ExecOutcome {
8858        ExecOutcome {
8859            variables: Default::default(),
8860            operations: Default::default(),
8861            artifact_graph: Default::default(),
8862            scene_objects: Default::default(),
8863            source_range_to_object: Default::default(),
8864            var_solutions,
8865            issues: Default::default(),
8866            filenames: Default::default(),
8867            default_planes: Default::default(),
8868        }
8869    }
8870
8871    /// Happy path: commit a var solution to a `var N` inside a sketch block
8872    /// using a correct NodePath. Confirms the node-path code path produces the
8873    /// expected source mutation.
8874    #[test]
8875    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
8876        let initial_source = "\
8877sketch(on = XY) {
8878  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8879}
8880";
8881        let program = Program::parse(initial_source).unwrap().0.unwrap();
8882        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
8883        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
8884        let (literal_range, node_path) = matches.into_iter().next().unwrap();
8885
8886        let mut frontend = FrontendState::new();
8887        frontend.program = program;
8888
8889        let outcome = empty_exec_outcome_with_var_solutions(vec![(
8890            literal_range,
8891            Some(node_path),
8892            Number {
8893                value: 25.0,
8894                units: NumericSuffix::Mm,
8895            },
8896        )]);
8897
8898        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8899
8900        assert_eq!(
8901            source_delta.text,
8902            "\
8903sketch(on = XY) {
8904  line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
8905}
8906",
8907        );
8908    }
8909
8910    /// Whitespace inserted earlier in the source shifts the original SketchVar
8911    /// SourceRange. With NodePath propagation the commit should still target
8912    /// the right `var`. We simulate this by collecting node_paths against a
8913    /// "compact" source, then loading the frontend with a "padded" source
8914    /// (whose byte offsets differ), and feeding the original (now stale)
8915    /// source range plus the correct node_path back into the commit.
8916    #[test]
8917    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
8918        let compact_source = "\
8919sketch(on = XY) {
8920  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8921}
8922";
8923        let padded_source = "\
8924// added comment\n// added comment\n\nsketch(on = XY) {
8925  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8926}
8927";
8928        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
8929        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
8930
8931        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
8932            .into_iter()
8933            .next()
8934            .expect("expected `var 10mm` in compact source");
8935        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
8936            .into_iter()
8937            .next()
8938            .expect("expected `var 10mm` in padded source");
8939
8940        assert_ne!(
8941            compact_match.0, padded_match.0,
8942            "byte offsets must differ for this test to be meaningful"
8943        );
8944        assert_eq!(
8945            compact_match.1, padded_match.1,
8946            "node paths must agree across whitespace; that's the whole point of NodePath",
8947        );
8948
8949        let mut frontend = FrontendState::new();
8950        frontend.program = padded_program;
8951
8952        // Stale source range from the compact source + correct node_path.
8953        let outcome = empty_exec_outcome_with_var_solutions(vec![(
8954            compact_match.0,
8955            Some(compact_match.1),
8956            Number {
8957                value: 30.0,
8958                units: NumericSuffix::Mm,
8959            },
8960        )]);
8961
8962        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8963
8964        assert_eq!(
8965            source_delta.text,
8966            "\
8967// added comment
8968// added comment
8969
8970sketch(on = XY) {
8971  line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
8972}
8973",
8974        );
8975    }
8976
8977    /// When multiple `var` declarations exist and the stale source range
8978    /// happens to land on a *different* var, the node_path must take
8979    /// precedence and the right var gets updated.
8980    #[test]
8981    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
8982        let initial_source = "\
8983sketch(on = XY) {
8984  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
8985}
8986";
8987        let program = Program::parse(initial_source).unwrap().0.unwrap();
8988
8989        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
8990            .into_iter()
8991            .next()
8992            .expect("expected `var 10mm`");
8993        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
8994            .into_iter()
8995            .next()
8996            .expect("expected `var 20mm`");
8997
8998        let mut frontend = FrontendState::new();
8999        frontend.program = program;
9000
9001        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9002        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9003            var_20.0,
9004            Some(var_10.1),
9005            Number {
9006                value: 33.0,
9007                units: NumericSuffix::Mm,
9008            },
9009        )]);
9010
9011        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9012
9013        assert_eq!(
9014            source_delta.text,
9015            "\
9016sketch(on = XY) {
9017  line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9018}
9019",
9020        );
9021    }
9022
9023    /// Bare `var` (no initial literal) is only locatable via node_path. With
9024    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9025    /// solver solution should fill the initial value in. The
9026    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9027    /// is gated as an experimental feature; without it the re-parse of the
9028    /// recast source rejects bare `var` declarations.
9029    #[test]
9030    fn test_commit_var_solution_writes_back_into_bare_var() {
9031        let initial_source = "\
9032@settings(experimentalFeatures = allow, kclVersion = 2.0)
9033sketch(on = XY) {
9034  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9035}
9036";
9037        let program = Program::parse(initial_source).unwrap().0.unwrap();
9038
9039        // Pick the first bare `var`; collect_all_sketch_vars returns every
9040        // SketchVar, including bare ones.
9041        let bare = collect_all_sketch_vars(&program)
9042            .into_iter()
9043            .find(|(range, _)| {
9044                // The bare `var` is exactly the 3 characters "var".
9045                range.end() - range.start() == 3
9046            })
9047            .expect("expected at least one bare `var`");
9048
9049        let mut frontend = FrontendState::new();
9050        frontend.program = program;
9051
9052        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9053            bare.0,
9054            Some(bare.1),
9055            Number {
9056                value: 7.0,
9057                units: NumericSuffix::Mm,
9058            },
9059        )]);
9060
9061        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9062
9063        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9064        // written as an explicit suffix so the bare var commits with units.
9065        // The recast adds a blank line after the `@settings` annotation.
9066        assert_eq!(
9067            source_delta.text,
9068            "\
9069@settings(experimentalFeatures = allow, kclVersion = 2.0)
9070
9071sketch(on = XY) {
9072  line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9073}
9074",
9075        );
9076    }
9077
9078    #[tokio::test(flavor = "multi_thread")]
9079    async fn test_delete_point_without_var() {
9080        let initial_source = "\
9081sketch(on = XY) {
9082  point(at = [var 1, var 2])
9083  point(at = [var 3, var 4])
9084  point(at = [var 5, var 6])
9085}
9086";
9087
9088        let program = Program::parse(initial_source).unwrap().0.unwrap();
9089
9090        let mut frontend = FrontendState::new();
9091
9092        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9093        let mock_ctx = ExecutorContext::new_mock(None).await;
9094        let version = Version(0);
9095
9096        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9097        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9098        let sketch_id = sketch_object.id;
9099        let sketch = expect_sketch(sketch_object);
9100
9101        let point_id = *sketch.segments.get(1).unwrap();
9102
9103        let (src_delta, scene_delta) = frontend
9104            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9105            .await
9106            .unwrap();
9107        assert_eq!(
9108            src_delta.text.as_str(),
9109            "\
9110sketch(on = XY) {
9111  point(at = [var 1, var 2])
9112  point(at = [var 5, var 6])
9113}
9114"
9115        );
9116        assert_eq!(scene_delta.new_objects, vec![]);
9117        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9118
9119        ctx.close().await;
9120        mock_ctx.close().await;
9121    }
9122
9123    #[tokio::test(flavor = "multi_thread")]
9124    async fn test_delete_point_with_var() {
9125        let initial_source = "\
9126sketch(on = XY) {
9127  point(at = [var 1, var 2])
9128  point1 = point(at = [var 3, var 4])
9129  point(at = [var 5, var 6])
9130}
9131";
9132
9133        let program = Program::parse(initial_source).unwrap().0.unwrap();
9134
9135        let mut frontend = FrontendState::new();
9136
9137        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9138        let mock_ctx = ExecutorContext::new_mock(None).await;
9139        let version = Version(0);
9140
9141        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9142        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9143        let sketch_id = sketch_object.id;
9144        let sketch = expect_sketch(sketch_object);
9145
9146        let point_id = *sketch.segments.get(1).unwrap();
9147
9148        let (src_delta, scene_delta) = frontend
9149            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9150            .await
9151            .unwrap();
9152        assert_eq!(
9153            src_delta.text.as_str(),
9154            "\
9155sketch(on = XY) {
9156  point(at = [var 1, var 2])
9157  point(at = [var 5, var 6])
9158}
9159"
9160        );
9161        assert_eq!(scene_delta.new_objects, vec![]);
9162        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9163
9164        ctx.close().await;
9165        mock_ctx.close().await;
9166    }
9167
9168    #[tokio::test(flavor = "multi_thread")]
9169    async fn test_delete_multiple_points() {
9170        let initial_source = "\
9171sketch(on = XY) {
9172  point(at = [var 1, var 2])
9173  point1 = point(at = [var 3, var 4])
9174  point(at = [var 5, var 6])
9175}
9176";
9177
9178        let program = Program::parse(initial_source).unwrap().0.unwrap();
9179
9180        let mut frontend = FrontendState::new();
9181
9182        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9183        let mock_ctx = ExecutorContext::new_mock(None).await;
9184        let version = Version(0);
9185
9186        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9187        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9188        let sketch_id = sketch_object.id;
9189
9190        let sketch = expect_sketch(sketch_object);
9191
9192        let point1_id = *sketch.segments.first().unwrap();
9193        let point2_id = *sketch.segments.get(1).unwrap();
9194
9195        let (src_delta, scene_delta) = frontend
9196            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9197            .await
9198            .unwrap();
9199        assert_eq!(
9200            src_delta.text.as_str(),
9201            "\
9202sketch(on = XY) {
9203  point(at = [var 5, var 6])
9204}
9205"
9206        );
9207        assert_eq!(scene_delta.new_objects, vec![]);
9208        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9209
9210        ctx.close().await;
9211        mock_ctx.close().await;
9212    }
9213
9214    #[tokio::test(flavor = "multi_thread")]
9215    async fn test_delete_coincident_constraint() {
9216        let initial_source = "\
9217sketch(on = XY) {
9218  point1 = point(at = [var 1, var 2])
9219  point2 = point(at = [var 3, var 4])
9220  coincident([point1, point2])
9221  point(at = [var 5, var 6])
9222}
9223";
9224
9225        let program = Program::parse(initial_source).unwrap().0.unwrap();
9226
9227        let mut frontend = FrontendState::new();
9228
9229        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9230        let mock_ctx = ExecutorContext::new_mock(None).await;
9231        let version = Version(0);
9232
9233        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9234        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9235        let sketch_id = sketch_object.id;
9236        let sketch = expect_sketch(sketch_object);
9237
9238        let coincident_id = *sketch.constraints.first().unwrap();
9239
9240        let (src_delta, scene_delta) = frontend
9241            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9242            .await
9243            .unwrap();
9244        assert_eq!(
9245            src_delta.text.as_str(),
9246            "\
9247sketch(on = XY) {
9248  point1 = point(at = [var 1, var 2])
9249  point2 = point(at = [var 3, var 4])
9250  point(at = [var 5, var 6])
9251}
9252"
9253        );
9254        assert_eq!(scene_delta.new_objects, vec![]);
9255        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9256
9257        ctx.close().await;
9258        mock_ctx.close().await;
9259    }
9260
9261    #[tokio::test(flavor = "multi_thread")]
9262    async fn test_delete_line_cascades_to_coincident_constraint() {
9263        let initial_source = "\
9264sketch(on = XY) {
9265  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9266  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9267  coincident([line1.end, line2.start])
9268}
9269";
9270
9271        let program = Program::parse(initial_source).unwrap().0.unwrap();
9272
9273        let mut frontend = FrontendState::new();
9274
9275        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9276        let mock_ctx = ExecutorContext::new_mock(None).await;
9277        let version = Version(0);
9278
9279        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9280        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9281        let sketch_id = sketch_object.id;
9282        let sketch = expect_sketch(sketch_object);
9283        let line_id = *sketch.segments.get(5).unwrap();
9284
9285        let (src_delta, scene_delta) = frontend
9286            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9287            .await
9288            .unwrap();
9289        assert_eq!(
9290            src_delta.text.as_str(),
9291            "\
9292sketch(on = XY) {
9293  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9294}
9295"
9296        );
9297        assert_eq!(
9298            scene_delta.new_graph.objects.len(),
9299            5,
9300            "{:#?}",
9301            scene_delta.new_graph.objects
9302        );
9303
9304        ctx.close().await;
9305        mock_ctx.close().await;
9306    }
9307
9308    #[tokio::test(flavor = "multi_thread")]
9309    async fn test_delete_line_cascades_to_distance_constraint() {
9310        let initial_source = "\
9311sketch(on = XY) {
9312  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9313  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9314  distance([line1.end, line2.start]) == 10mm
9315}
9316";
9317
9318        let program = Program::parse(initial_source).unwrap().0.unwrap();
9319
9320        let mut frontend = FrontendState::new();
9321
9322        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9323        let mock_ctx = ExecutorContext::new_mock(None).await;
9324        let version = Version(0);
9325
9326        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9327        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9328        let sketch_id = sketch_object.id;
9329        let sketch = expect_sketch(sketch_object);
9330        let line_id = *sketch.segments.get(5).unwrap();
9331
9332        let (src_delta, scene_delta) = frontend
9333            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9334            .await
9335            .unwrap();
9336        assert_eq!(
9337            src_delta.text.as_str(),
9338            "\
9339sketch(on = XY) {
9340  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9341}
9342"
9343        );
9344        assert_eq!(
9345            scene_delta.new_graph.objects.len(),
9346            5,
9347            "{:#?}",
9348            scene_delta.new_graph.objects
9349        );
9350
9351        ctx.close().await;
9352        mock_ctx.close().await;
9353    }
9354
9355    #[tokio::test(flavor = "multi_thread")]
9356    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9357        let initial_source = "\
9358sketch(on = XY) {
9359  point1 = point(at = [var 1, var 2])
9360  point2 = point(at = [var 3, var 4])
9361  horizontalDistance([point1, point2]) == 10mm
9362}
9363";
9364
9365        let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367        let mut frontend = FrontendState::new();
9368
9369        let mock_ctx = ExecutorContext::new_mock(None).await;
9370        let version = Version(0);
9371
9372        frontend.program = program.clone();
9373        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9374        frontend.update_state_after_exec(outcome, true);
9375        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9376        let sketch_id = sketch_object.id;
9377        let sketch = expect_sketch(sketch_object);
9378        let point2_id = *sketch.segments.get(1).unwrap();
9379
9380        let (src_delta, scene_delta) = frontend
9381            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9382            .await
9383            .unwrap();
9384        assert_eq!(
9385            src_delta.text.as_str(),
9386            "\
9387sketch(on = XY) {
9388  point1 = point(at = [var 1, var 2])
9389}
9390"
9391        );
9392        assert_eq!(
9393            scene_delta.new_graph.objects.len(),
9394            3,
9395            "{:#?}",
9396            scene_delta.new_graph.objects
9397        );
9398
9399        mock_ctx.close().await;
9400    }
9401
9402    #[tokio::test(flavor = "multi_thread")]
9403    async fn test_delete_line_cascades_to_fixed_constraint() {
9404        let initial_source = "\
9405sketch(on = XY) {
9406  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9407  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9408  fixed([line1.start, [0, 0]])
9409}
9410";
9411
9412        let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414        let mut frontend = FrontendState::new();
9415
9416        let mock_ctx = ExecutorContext::new_mock(None).await;
9417        let version = Version(0);
9418
9419        frontend.program = program.clone();
9420        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9421        frontend.update_state_after_exec(outcome, true);
9422        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9423        let sketch_id = sketch_object.id;
9424        let sketch = expect_sketch(sketch_object);
9425        let line1_id = *sketch.segments.get(2).unwrap();
9426
9427        let (src_delta, scene_delta) = frontend
9428            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9429            .await
9430            .unwrap();
9431        assert_eq!(
9432            src_delta.text.as_str(),
9433            "\
9434sketch(on = XY) {
9435  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9436}
9437"
9438        );
9439        assert_eq!(
9440            scene_delta.new_graph.objects.len(),
9441            5,
9442            "{:#?}",
9443            scene_delta.new_graph.objects
9444        );
9445
9446        mock_ctx.close().await;
9447    }
9448
9449    #[tokio::test(flavor = "multi_thread")]
9450    async fn test_delete_line_cascades_to_midpoint_constraint() {
9451        let initial_source = "\
9452sketch(on = XY) {
9453  point1 = point(at = [var 1, var 2])
9454  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9455  midpoint(line1, point = point1)
9456}
9457";
9458
9459        let program = Program::parse(initial_source).unwrap().0.unwrap();
9460
9461        let mut frontend = FrontendState::new();
9462
9463        let mock_ctx = ExecutorContext::new_mock(None).await;
9464        let version = Version(0);
9465
9466        frontend.program = program.clone();
9467        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9468        frontend.update_state_after_exec(outcome, true);
9469        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9470        let sketch_id = sketch_object.id;
9471        let sketch = expect_sketch(sketch_object);
9472        let line1_id = *sketch.segments.get(3).unwrap();
9473
9474        let (src_delta, scene_delta) = frontend
9475            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9476            .await
9477            .unwrap();
9478        assert_eq!(
9479            src_delta.text.as_str(),
9480            "\
9481sketch(on = XY) {
9482  point1 = point(at = [var 1, var 2])
9483}
9484"
9485        );
9486        assert_eq!(
9487            scene_delta.new_graph.objects.len(),
9488            3,
9489            "{:#?}",
9490            scene_delta.new_graph.objects
9491        );
9492
9493        mock_ctx.close().await;
9494    }
9495
9496    #[tokio::test(flavor = "multi_thread")]
9497    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9498        let initial_source = "\
9499sketch(on = XY) {
9500  point1 = point(at = [var 1, var 2])
9501  point2 = point(at = [var 3, var 4])
9502  point3 = point(at = [var 5, var 6])
9503  coincident([point1, point2, point3])
9504}
9505";
9506
9507        let program = Program::parse(initial_source).unwrap().0.unwrap();
9508
9509        let mut frontend = FrontendState::new();
9510
9511        let mock_ctx = ExecutorContext::new_mock(None).await;
9512        let version = Version(0);
9513
9514        frontend.program = program.clone();
9515        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9516        frontend.update_state_after_exec(outcome, true);
9517        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9518        let sketch_id = sketch_object.id;
9519        let sketch = expect_sketch(sketch_object);
9520        let point3_id = *sketch.segments.get(2).unwrap();
9521
9522        let (src_delta, scene_delta) = frontend
9523            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9524            .await
9525            .unwrap();
9526        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9527        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9528        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9529        assert!(
9530            src_delta.text.contains("coincident([point1, point2])"),
9531            "{}",
9532            src_delta.text
9533        );
9534
9535        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9536        let sketch = expect_sketch(sketch_object);
9537        assert_eq!(sketch.segments.len(), 2);
9538        assert_eq!(sketch.constraints.len(), 1);
9539
9540        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9541        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9542            panic!("Expected constraint object");
9543        };
9544        let Constraint::Coincident(coincident) = constraint else {
9545            panic!("Expected coincident constraint");
9546        };
9547        assert_eq!(
9548            coincident.segments,
9549            sketch
9550                .segments
9551                .iter()
9552                .copied()
9553                .map(Into::into)
9554                .collect::<Vec<ConstraintSegment>>()
9555        );
9556
9557        mock_ctx.close().await;
9558    }
9559
9560    #[tokio::test(flavor = "multi_thread")]
9561    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9562        let initial_source = "\
9563sketch(on = XY) {
9564  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9565  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9566  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9567  equalLength([line1, line2, line3])
9568}
9569";
9570
9571        let program = Program::parse(initial_source).unwrap().0.unwrap();
9572
9573        let mut frontend = FrontendState::new();
9574
9575        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9576        let mock_ctx = ExecutorContext::new_mock(None).await;
9577        let version = Version(0);
9578
9579        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9580        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9581        let sketch_id = sketch_object.id;
9582        let sketch = expect_sketch(sketch_object);
9583        let line3_id = *sketch.segments.get(8).unwrap();
9584
9585        let (src_delta, scene_delta) = frontend
9586            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9587            .await
9588            .unwrap();
9589        assert_eq!(
9590            src_delta.text.as_str(),
9591            "\
9592sketch(on = XY) {
9593  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9594  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9595  equalLength([line1, line2])
9596}
9597"
9598        );
9599
9600        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9601        let sketch = expect_sketch(sketch_object);
9602        assert_eq!(sketch.constraints.len(), 1);
9603
9604        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9605        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9606            panic!("Expected constraint object");
9607        };
9608        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9609            panic!("Expected lines equal length constraint");
9610        };
9611        assert_eq!(lines_equal_length.lines.len(), 2);
9612
9613        ctx.close().await;
9614        mock_ctx.close().await;
9615    }
9616
9617    #[tokio::test(flavor = "multi_thread")]
9618    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9619        let initial_source = "\
9620sketch(on = XY) {
9621  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9622  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9623  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9624  horizontal([line1.end, line2.start, line3.start])
9625}
9626";
9627
9628        let program = Program::parse(initial_source).unwrap().0.unwrap();
9629
9630        let mut frontend = FrontendState::new();
9631
9632        let mock_ctx = ExecutorContext::new_mock(None).await;
9633        let version = Version(0);
9634
9635        frontend.program = program.clone();
9636        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9637        frontend.update_state_after_exec(outcome, true);
9638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9639        let sketch_id = sketch_object.id;
9640        let sketch = expect_sketch(sketch_object);
9641        let line1_id = *sketch.segments.get(2).unwrap();
9642
9643        let (src_delta, scene_delta) = frontend
9644            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9645            .await
9646            .unwrap();
9647        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9648        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9649        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9650        assert!(
9651            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9652            "{}",
9653            src_delta.text
9654        );
9655
9656        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9657        let sketch = expect_sketch(sketch_object);
9658        assert_eq!(sketch.constraints.len(), 1);
9659
9660        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9661        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9662            panic!("Expected constraint object");
9663        };
9664        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9665            panic!("Expected horizontal points constraint");
9666        };
9667        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9668        assert_eq!(*points, remaining_points);
9669
9670        mock_ctx.close().await;
9671    }
9672
9673    #[tokio::test(flavor = "multi_thread")]
9674    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9675        let initial_source = "\
9676sketch(on = XY) {
9677  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9678  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9679  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9680  vertical([line1.end, line2.start, line3.start])
9681}
9682";
9683
9684        let program = Program::parse(initial_source).unwrap().0.unwrap();
9685
9686        let mut frontend = FrontendState::new();
9687
9688        let mock_ctx = ExecutorContext::new_mock(None).await;
9689        let version = Version(0);
9690
9691        frontend.program = program.clone();
9692        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9693        frontend.update_state_after_exec(outcome, true);
9694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9695        let sketch_id = sketch_object.id;
9696        let sketch = expect_sketch(sketch_object);
9697        let line1_id = *sketch.segments.get(2).unwrap();
9698
9699        let (src_delta, scene_delta) = frontend
9700            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9701            .await
9702            .unwrap();
9703        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9704        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9705        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9706        assert!(
9707            src_delta.text.contains("vertical([line2.start, line3.start])"),
9708            "{}",
9709            src_delta.text
9710        );
9711
9712        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9713        let sketch = expect_sketch(sketch_object);
9714        assert_eq!(sketch.constraints.len(), 1);
9715
9716        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9717        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9718            panic!("Expected constraint object");
9719        };
9720        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9721            panic!("Expected vertical points constraint");
9722        };
9723        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9724        assert_eq!(*points, remaining_points);
9725
9726        mock_ctx.close().await;
9727    }
9728
9729    #[tokio::test(flavor = "multi_thread")]
9730    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9731        let initial_source = "\
9732sketch(on = XY) {
9733  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9734  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9735  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9736  coincident([line1.end, line2.start, line3.start])
9737}
9738";
9739
9740        let program = Program::parse(initial_source).unwrap().0.unwrap();
9741
9742        let mut frontend = FrontendState::new();
9743
9744        let mock_ctx = ExecutorContext::new_mock(None).await;
9745        let version = Version(0);
9746
9747        frontend.program = program.clone();
9748        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9749        frontend.update_state_after_exec(outcome, true);
9750        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9751        let sketch_id = sketch_object.id;
9752        let sketch = expect_sketch(sketch_object);
9753        let line1_id = *sketch.segments.get(2).unwrap();
9754
9755        let (src_delta, scene_delta) = frontend
9756            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9757            .await
9758            .unwrap();
9759        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9760        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9761        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9762        assert!(
9763            src_delta.text.contains("coincident([line2.start, line3.start])"),
9764            "{}",
9765            src_delta.text
9766        );
9767
9768        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9769        let sketch = expect_sketch(sketch_object);
9770        assert_eq!(sketch.constraints.len(), 1);
9771
9772        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9773        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9774            panic!("Expected constraint object");
9775        };
9776        let Constraint::Coincident(coincident) = constraint else {
9777            panic!("Expected coincident constraint");
9778        };
9779        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9780        assert_eq!(coincident.segments, remaining_segments);
9781
9782        mock_ctx.close().await;
9783    }
9784
9785    #[tokio::test(flavor = "multi_thread")]
9786    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9787        let initial_source = "\
9788sketch(on = XY) {
9789  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9790  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9791  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9792  equalLength([line1, line2, line3])
9793}
9794";
9795
9796        let program = Program::parse(initial_source).unwrap().0.unwrap();
9797
9798        let mut frontend = FrontendState::new();
9799
9800        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9801        let mock_ctx = ExecutorContext::new_mock(None).await;
9802        let version = Version(0);
9803
9804        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9805        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9806        let sketch_id = sketch_object.id;
9807        let sketch = expect_sketch(sketch_object);
9808        let line2_id = *sketch.segments.get(5).unwrap();
9809        let line3_id = *sketch.segments.get(8).unwrap();
9810
9811        let (src_delta, scene_delta) = frontend
9812            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9813            .await
9814            .unwrap();
9815        assert_eq!(
9816            src_delta.text.as_str(),
9817            "\
9818sketch(on = XY) {
9819  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820}
9821"
9822        );
9823
9824        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9825        let sketch = expect_sketch(sketch_object);
9826        assert!(sketch.constraints.is_empty());
9827
9828        ctx.close().await;
9829        mock_ctx.close().await;
9830    }
9831
9832    #[tokio::test(flavor = "multi_thread")]
9833    async fn test_delete_line_preserves_multiline_parallel_constraint() {
9834        let initial_source = "\
9835sketch(on = XY) {
9836  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9837  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9838  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9839  parallel([line1, line2, line3])
9840}
9841";
9842
9843        let program = Program::parse(initial_source).unwrap().0.unwrap();
9844
9845        let mut frontend = FrontendState::new();
9846
9847        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9848        let mock_ctx = ExecutorContext::new_mock(None).await;
9849        let version = Version(0);
9850
9851        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9852        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9853        let sketch_id = sketch_object.id;
9854        let sketch = expect_sketch(sketch_object);
9855        let line3_id = *sketch.segments.get(8).unwrap();
9856
9857        let (src_delta, scene_delta) = frontend
9858            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9859            .await
9860            .unwrap();
9861        assert_eq!(
9862            src_delta.text.as_str(),
9863            "\
9864sketch(on = XY) {
9865  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9866  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9867  parallel([line1, line2])
9868}
9869"
9870        );
9871
9872        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9873        let sketch = expect_sketch(sketch_object);
9874        assert_eq!(sketch.constraints.len(), 1);
9875
9876        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9877        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9878            panic!("Expected constraint object");
9879        };
9880        let Constraint::Parallel(parallel) = constraint else {
9881            panic!("Expected parallel constraint");
9882        };
9883        assert_eq!(parallel.lines.len(), 2);
9884
9885        ctx.close().await;
9886        mock_ctx.close().await;
9887    }
9888
9889    #[tokio::test(flavor = "multi_thread")]
9890    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
9891        let initial_source = "\
9892sketch(on = XY) {
9893  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9894  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9895  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9896  parallel([line1, line2, line3])
9897}
9898";
9899
9900        let program = Program::parse(initial_source).unwrap().0.unwrap();
9901
9902        let mut frontend = FrontendState::new();
9903
9904        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9905        let mock_ctx = ExecutorContext::new_mock(None).await;
9906        let version = Version(0);
9907
9908        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9909        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9910        let sketch_id = sketch_object.id;
9911        let sketch = expect_sketch(sketch_object);
9912        let line2_id = *sketch.segments.get(5).unwrap();
9913        let line3_id = *sketch.segments.get(8).unwrap();
9914
9915        let (src_delta, scene_delta) = frontend
9916            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9917            .await
9918            .unwrap();
9919        assert_eq!(
9920            src_delta.text.as_str(),
9921            "\
9922sketch(on = XY) {
9923  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9924}
9925"
9926        );
9927
9928        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9929        let sketch = expect_sketch(sketch_object);
9930        assert!(sketch.constraints.is_empty());
9931
9932        ctx.close().await;
9933        mock_ctx.close().await;
9934    }
9935
9936    #[tokio::test(flavor = "multi_thread")]
9937    async fn test_delete_line_line_coincident_constraint() {
9938        let initial_source = "\
9939sketch(on = XY) {
9940  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9941  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9942  coincident([line1, line2])
9943}
9944";
9945
9946        let program = Program::parse(initial_source).unwrap().0.unwrap();
9947
9948        let mut frontend = FrontendState::new();
9949
9950        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9951        let mock_ctx = ExecutorContext::new_mock(None).await;
9952        let version = Version(0);
9953
9954        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9955        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9956        let sketch_id = sketch_object.id;
9957        let sketch = expect_sketch(sketch_object);
9958
9959        let coincident_id = *sketch.constraints.first().unwrap();
9960
9961        let (src_delta, scene_delta) = frontend
9962            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9963            .await
9964            .unwrap();
9965        assert_eq!(
9966            src_delta.text.as_str(),
9967            "\
9968sketch(on = XY) {
9969  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9970  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9971}
9972"
9973        );
9974        assert_eq!(scene_delta.new_objects, vec![]);
9975        assert_eq!(scene_delta.new_graph.objects.len(), 8);
9976
9977        ctx.close().await;
9978        mock_ctx.close().await;
9979    }
9980
9981    #[tokio::test(flavor = "multi_thread")]
9982    async fn test_two_points_coincident() {
9983        let initial_source = "\
9984sketch(on = XY) {
9985  point1 = point(at = [var 1, var 2])
9986  point(at = [3, 4])
9987}
9988";
9989
9990        let program = Program::parse(initial_source).unwrap().0.unwrap();
9991
9992        let mut frontend = FrontendState::new();
9993
9994        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9995        let mock_ctx = ExecutorContext::new_mock(None).await;
9996        let version = Version(0);
9997
9998        frontend.hack_set_program(&ctx, program).await.unwrap();
9999        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10000        let sketch_id = sketch_object.id;
10001        let sketch = expect_sketch(sketch_object);
10002        let point0_id = *sketch.segments.first().unwrap();
10003        let point1_id = *sketch.segments.get(1).unwrap();
10004
10005        let constraint = Constraint::Coincident(Coincident {
10006            segments: vec![point0_id.into(), point1_id.into()],
10007        });
10008        let (src_delta, scene_delta) = frontend
10009            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10010            .await
10011            .unwrap();
10012        assert_eq!(
10013            src_delta.text.as_str(),
10014            "\
10015sketch(on = XY) {
10016  point1 = point(at = [var 3, var 4])
10017  point2 = point(at = [3, 4])
10018  coincident([point1, point2])
10019}
10020"
10021        );
10022        assert_eq!(
10023            scene_delta.new_graph.objects.len(),
10024            5,
10025            "{:#?}",
10026            scene_delta.new_graph.objects
10027        );
10028
10029        ctx.close().await;
10030        mock_ctx.close().await;
10031    }
10032
10033    #[tokio::test(flavor = "multi_thread")]
10034    async fn test_three_points_coincident() {
10035        let initial_source = "\
10036sketch(on = XY) {
10037  point1 = point(at = [var 1, var 2])
10038  point(at = [var 3, var 4])
10039  point(at = [var 5, var 6])
10040}
10041";
10042
10043        let program = Program::parse(initial_source).unwrap().0.unwrap();
10044
10045        let mut frontend = FrontendState::new();
10046
10047        let mock_ctx = ExecutorContext::new_mock(None).await;
10048        let version = Version(0);
10049
10050        frontend.program = program.clone();
10051        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10052        frontend.update_state_after_exec(outcome, true);
10053        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10054        let sketch_id = sketch_object.id;
10055        let sketch = expect_sketch(sketch_object);
10056        let segments = sketch
10057            .segments
10058            .iter()
10059            .take(3)
10060            .copied()
10061            .map(Into::into)
10062            .collect::<Vec<ConstraintSegment>>();
10063
10064        let constraint = Constraint::Coincident(Coincident {
10065            segments: segments.clone(),
10066        });
10067        let (src_delta, scene_delta) = frontend
10068            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10069            .await
10070            .unwrap();
10071        assert_eq!(
10072            src_delta.text.as_str(),
10073            "\
10074sketch(on = XY) {
10075  point1 = point(at = [var 3, var 4])
10076  point2 = point(at = [var 3, var 4])
10077  point3 = point(at = [var 3, var 4])
10078  coincident([point1, point2, point3])
10079}
10080"
10081        );
10082
10083        let constraint_object = scene_delta
10084            .new_graph
10085            .objects
10086            .iter()
10087            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10088            .unwrap();
10089
10090        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10091            panic!("expected a constraint object");
10092        };
10093
10094        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10095
10096        mock_ctx.close().await;
10097    }
10098
10099    #[tokio::test(flavor = "multi_thread")]
10100    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10101        let initial_source = "\
10102sketch(on = XY) {
10103  point1 = point(at = [var 1, var 2])
10104  point2 = point(at = [var 3, var 4])
10105  point3 = point(at = [var 5, var 6])
10106  coincident([point1, point2, point3])
10107}
10108";
10109
10110        let program = Program::parse(initial_source).unwrap().0.unwrap();
10111
10112        let mut frontend = FrontendState::new();
10113
10114        let ctx = ExecutorContext::new_mock(None).await;
10115        frontend.program = program.clone();
10116        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10117        frontend.update_state_after_exec(outcome, true);
10118
10119        let constraint_object = frontend
10120            .scene_graph
10121            .objects
10122            .iter()
10123            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10124            .unwrap();
10125        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10126            panic!("expected a constraint object");
10127        };
10128
10129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10130        let sketch = expect_sketch(sketch_object);
10131        let expected_segments = sketch
10132            .segments
10133            .iter()
10134            .take(3)
10135            .copied()
10136            .map(Into::into)
10137            .collect::<Vec<ConstraintSegment>>();
10138
10139        assert_eq!(
10140            constraint,
10141            &Constraint::Coincident(Coincident {
10142                segments: expected_segments,
10143            })
10144        );
10145
10146        ctx.close().await;
10147    }
10148
10149    #[tokio::test(flavor = "multi_thread")]
10150    async fn test_point_origin_coincident_preserves_order() {
10151        let initial_source = "\
10152sketch(on = XY) {
10153  point(at = [var 1, var 2])
10154}
10155";
10156
10157        for (origin_first, expected_source) in [
10158            (
10159                true,
10160                "\
10161sketch(on = XY) {
10162  point1 = point(at = [var 0, var 0])
10163  coincident([ORIGIN, point1])
10164}
10165",
10166            ),
10167            (
10168                false,
10169                "\
10170sketch(on = XY) {
10171  point1 = point(at = [var 0, var 0])
10172  coincident([point1, ORIGIN])
10173}
10174",
10175            ),
10176        ] {
10177            let program = Program::parse(initial_source).unwrap().0.unwrap();
10178
10179            let mut frontend = FrontendState::new();
10180
10181            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10182            let mock_ctx = ExecutorContext::new_mock(None).await;
10183            let version = Version(0);
10184
10185            frontend.hack_set_program(&ctx, program).await.unwrap();
10186            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10187            let sketch_id = sketch_object.id;
10188            let sketch = expect_sketch(sketch_object);
10189            let point_id = *sketch.segments.first().unwrap();
10190
10191            let segments = if origin_first {
10192                vec![ConstraintSegment::ORIGIN, point_id.into()]
10193            } else {
10194                vec![point_id.into(), ConstraintSegment::ORIGIN]
10195            };
10196            let constraint = Constraint::Coincident(Coincident {
10197                segments: segments.clone(),
10198            });
10199            let (src_delta, scene_delta) = frontend
10200                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10201                .await
10202                .unwrap();
10203            assert_eq!(src_delta.text.as_str(), expected_source);
10204
10205            let constraint_object = scene_delta
10206                .new_graph
10207                .objects
10208                .iter()
10209                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10210                .unwrap();
10211
10212            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10213                panic!("expected a constraint object");
10214            };
10215
10216            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10217
10218            ctx.close().await;
10219            mock_ctx.close().await;
10220        }
10221    }
10222
10223    #[tokio::test(flavor = "multi_thread")]
10224    async fn test_coincident_of_line_end_points() {
10225        let initial_source = "\
10226sketch(on = XY) {
10227  line(start = [var 1, var 2], end = [var 3, var 4])
10228  line(start = [var 5, var 6], end = [var 7, var 8])
10229}
10230";
10231
10232        let program = Program::parse(initial_source).unwrap().0.unwrap();
10233
10234        let mut frontend = FrontendState::new();
10235
10236        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237        let mock_ctx = ExecutorContext::new_mock(None).await;
10238        let version = Version(0);
10239
10240        frontend.hack_set_program(&ctx, program).await.unwrap();
10241        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10242        let sketch_id = sketch_object.id;
10243        let sketch = expect_sketch(sketch_object);
10244        let point0_id = *sketch.segments.get(1).unwrap();
10245        let point1_id = *sketch.segments.get(3).unwrap();
10246
10247        let constraint = Constraint::Coincident(Coincident {
10248            segments: vec![point0_id.into(), point1_id.into()],
10249        });
10250        let (src_delta, scene_delta) = frontend
10251            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10252            .await
10253            .unwrap();
10254        assert_eq!(
10255            src_delta.text.as_str(),
10256            "\
10257sketch(on = XY) {
10258  line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10259  line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10260  coincident([line1.end, line2.start])
10261}
10262"
10263        );
10264        assert_eq!(
10265            scene_delta.new_graph.objects.len(),
10266            9,
10267            "{:#?}",
10268            scene_delta.new_graph.objects
10269        );
10270
10271        ctx.close().await;
10272        mock_ctx.close().await;
10273    }
10274
10275    #[tokio::test(flavor = "multi_thread")]
10276    async fn test_coincident_of_line_point_and_circle_segment() {
10277        let initial_source = "\
10278sketch(on = XY) {
10279  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10280  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10281}
10282";
10283        let program = Program::parse(initial_source).unwrap().0.unwrap();
10284        let mut frontend = FrontendState::new();
10285
10286        let mock_ctx = ExecutorContext::new_mock(None).await;
10287        let version = Version(0);
10288
10289        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10290        frontend.program = program;
10291        frontend.update_state_after_exec(outcome, true);
10292        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10293        let sketch_id = sketch_object.id;
10294        let sketch = expect_sketch(sketch_object);
10295
10296        let circle_id = sketch
10297            .segments
10298            .iter()
10299            .copied()
10300            .find(|seg_id| {
10301                matches!(
10302                    &frontend.scene_graph.objects[seg_id.0].kind,
10303                    ObjectKind::Segment {
10304                        segment: Segment::Circle(_)
10305                    }
10306                )
10307            })
10308            .expect("Expected a circle segment in sketch");
10309        let line_id = frontend
10310            .scene_graph
10311            .objects
10312            .iter()
10313            .find_map(|obj| match &obj.kind {
10314                ObjectKind::Segment {
10315                    segment: Segment::Line(line),
10316                } if line.owner.is_none() => Some(obj.id),
10317                _ => None,
10318            })
10319            .expect("Expected a standalone line segment in scene graph");
10320
10321        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10322            ObjectKind::Segment {
10323                segment: Segment::Line(line),
10324            } => line.start,
10325            _ => panic!("Expected line segment object"),
10326        };
10327
10328        let constraint = Constraint::Coincident(Coincident {
10329            segments: vec![line_start_point_id.into(), circle_id.into()],
10330        });
10331        let (src_delta, _scene_delta) = frontend
10332            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10333            .await
10334            .unwrap();
10335        assert_eq!(
10336            src_delta.text.as_str(),
10337            "\
10338sketch(on = XY) {
10339  circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10340  line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10341  coincident([line1.start, circle1])
10342}
10343"
10344        );
10345
10346        mock_ctx.close().await;
10347    }
10348
10349    #[tokio::test(flavor = "multi_thread")]
10350    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10351        // Test that attempting an invalid coincident constraint (arc and line)
10352        // doesn't corrupt the state, allowing subsequent operations to work.
10353        // This test verifies the transactional fix in add_constraint that prevents
10354        // state corruption when invalid constraints are attempted.
10355        // Example: coincident constraint between an arc segment and a straight line segment
10356        // is geometrically invalid and should fail, but state should remain intact.
10357        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10358        let program = Program::empty();
10359
10360        let mut frontend = FrontendState::new();
10361        frontend.program = program;
10362
10363        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10364        let mock_ctx = ExecutorContext::new_mock(None).await;
10365        let version = Version(0);
10366
10367        let sketch_args = SketchCtor {
10368            on: Plane::Default(PlaneName::Xy),
10369        };
10370        let (_src_delta, _scene_delta, sketch_id) = frontend
10371            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10372            .await
10373            .unwrap();
10374
10375        // Add an arc segment
10376        let arc_ctor = ArcCtor {
10377            start: Point2d {
10378                x: Expr::Var(Number {
10379                    value: 0.0,
10380                    units: NumericSuffix::Mm,
10381                }),
10382                y: Expr::Var(Number {
10383                    value: 0.0,
10384                    units: NumericSuffix::Mm,
10385                }),
10386            },
10387            end: Point2d {
10388                x: Expr::Var(Number {
10389                    value: 10.0,
10390                    units: NumericSuffix::Mm,
10391                }),
10392                y: Expr::Var(Number {
10393                    value: 10.0,
10394                    units: NumericSuffix::Mm,
10395                }),
10396            },
10397            center: Point2d {
10398                x: Expr::Var(Number {
10399                    value: 10.0,
10400                    units: NumericSuffix::Mm,
10401                }),
10402                y: Expr::Var(Number {
10403                    value: 0.0,
10404                    units: NumericSuffix::Mm,
10405                }),
10406            },
10407            construction: None,
10408        };
10409        let (_src_delta, scene_delta) = frontend
10410            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10411            .await
10412            .unwrap();
10413        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10414        let arc_id = *scene_delta.new_objects.last().unwrap();
10415
10416        // Add a line segment
10417        let line_ctor = LineCtor {
10418            start: Point2d {
10419                x: Expr::Var(Number {
10420                    value: 20.0,
10421                    units: NumericSuffix::Mm,
10422                }),
10423                y: Expr::Var(Number {
10424                    value: 0.0,
10425                    units: NumericSuffix::Mm,
10426                }),
10427            },
10428            end: Point2d {
10429                x: Expr::Var(Number {
10430                    value: 30.0,
10431                    units: NumericSuffix::Mm,
10432                }),
10433                y: Expr::Var(Number {
10434                    value: 10.0,
10435                    units: NumericSuffix::Mm,
10436                }),
10437            },
10438            construction: None,
10439        };
10440        let (_src_delta, scene_delta) = frontend
10441            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10442            .await
10443            .unwrap();
10444        // The line is the last object in new_objects (after the 2 points: start, end)
10445        let line_id = *scene_delta.new_objects.last().unwrap();
10446
10447        // Attempt to add an invalid coincident constraint between arc and line
10448        // This should fail during execution, but state should remain intact
10449        let constraint = Constraint::Coincident(Coincident {
10450            segments: vec![arc_id.into(), line_id.into()],
10451        });
10452        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10453
10454        // The constraint addition should fail (invalid constraint)
10455        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10456
10457        // Verify state is not corrupted by checking that we can still access the scene graph
10458        // and that the original segments are still present with their source ranges
10459        let sketch_object_after =
10460            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10461        let sketch_after = expect_sketch(sketch_object_after);
10462
10463        // Verify both segments are still in the sketch
10464        assert!(
10465            sketch_after.segments.contains(&arc_id),
10466            "Arc segment should still exist after failed constraint"
10467        );
10468        assert!(
10469            sketch_after.segments.contains(&line_id),
10470            "Line segment should still exist after failed constraint"
10471        );
10472
10473        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10474        let arc_obj = frontend
10475            .scene_graph
10476            .objects
10477            .get(arc_id.0)
10478            .expect("Arc object should still be accessible");
10479        let line_obj = frontend
10480            .scene_graph
10481            .objects
10482            .get(line_id.0)
10483            .expect("Line object should still be accessible");
10484
10485        // Verify source ranges are still valid (not corrupted)
10486        // Just verify that the objects are still accessible and have the expected types
10487        match &arc_obj.kind {
10488            ObjectKind::Segment {
10489                segment: Segment::Arc(_),
10490            } => {}
10491            _ => panic!("Arc object should still be an arc segment"),
10492        }
10493        match &line_obj.kind {
10494            ObjectKind::Segment {
10495                segment: Segment::Line(_),
10496            } => {}
10497            _ => panic!("Line object should still be a line segment"),
10498        }
10499
10500        ctx.close().await;
10501        mock_ctx.close().await;
10502    }
10503
10504    #[tokio::test(flavor = "multi_thread")]
10505    async fn test_distance_two_points() {
10506        let initial_source = "\
10507sketch(on = XY) {
10508  point(at = [var 1, var 2])
10509  point(at = [var 3, var 4])
10510}
10511";
10512
10513        let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515        let mut frontend = FrontendState::new();
10516
10517        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10518        let mock_ctx = ExecutorContext::new_mock(None).await;
10519        let version = Version(0);
10520
10521        frontend.hack_set_program(&ctx, program).await.unwrap();
10522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523        let sketch_id = sketch_object.id;
10524        let sketch = expect_sketch(sketch_object);
10525        let point0_id = *sketch.segments.first().unwrap();
10526        let point1_id = *sketch.segments.get(1).unwrap();
10527
10528        let constraint = Constraint::Distance(Distance {
10529            points: vec![point0_id.into(), point1_id.into()],
10530            distance: Number {
10531                value: 2.0,
10532                units: NumericSuffix::Mm,
10533            },
10534            label_position: None,
10535            source: Default::default(),
10536        });
10537        let (src_delta, scene_delta) = frontend
10538            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10539            .await
10540            .unwrap();
10541        assert_eq!(
10542            src_delta.text.as_str(),
10543            // The lack indentation is a formatter bug.
10544            "\
10545sketch(on = XY) {
10546  point1 = point(at = [var 1.29, var 2.29])
10547  point2 = point(at = [var 2.71, var 3.71])
10548  distance([point1, point2]) == 2mm
10549}
10550"
10551        );
10552        assert_eq!(
10553            scene_delta.new_graph.objects.len(),
10554            5,
10555            "{:#?}",
10556            scene_delta.new_graph.objects
10557        );
10558
10559        ctx.close().await;
10560        mock_ctx.close().await;
10561    }
10562
10563    #[tokio::test(flavor = "multi_thread")]
10564    async fn test_distance_two_points_with_label() {
10565        let initial_source = "\
10566sketch(on = XY) {
10567  point(at = [var 1, var 2])
10568  point(at = [var 3, var 4])
10569}
10570";
10571
10572        let program = Program::parse(initial_source).unwrap().0.unwrap();
10573
10574        let mut frontend = FrontendState::new();
10575
10576        let mock_ctx = ExecutorContext::new_mock(None).await;
10577        let version = Version(0);
10578
10579        frontend.program = program.clone();
10580        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10581        frontend.update_state_after_exec(outcome, true);
10582        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10583        let sketch_id = sketch_object.id;
10584        let sketch = expect_sketch(sketch_object);
10585        let point0_id = *sketch.segments.first().unwrap();
10586        let point1_id = *sketch.segments.get(1).unwrap();
10587
10588        let label_position = Point2d {
10589            x: Number {
10590                value: 10.0,
10591                units: NumericSuffix::Mm,
10592            },
10593            y: Number {
10594                value: 11.0,
10595                units: NumericSuffix::Mm,
10596            },
10597        };
10598        let constraint = Constraint::Distance(Distance {
10599            points: vec![point0_id.into(), point1_id.into()],
10600            distance: Number {
10601                value: 2.0,
10602                units: NumericSuffix::Mm,
10603            },
10604            label_position: Some(label_position.clone()),
10605            source: Default::default(),
10606        });
10607        let (src_delta, scene_delta) = frontend
10608            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10609            .await
10610            .unwrap();
10611        assert_eq!(
10612            src_delta.text.as_str(),
10613            "\
10614sketch(on = XY) {
10615  point1 = point(at = [var 1.29, var 2.29])
10616  point2 = point(at = [var 2.71, var 3.71])
10617  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10618}
10619"
10620        );
10621
10622        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10623        let sketch = expect_sketch(sketch_object);
10624        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10625        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10626            panic!("Expected constraint object");
10627        };
10628        let Constraint::Distance(distance) = constraint else {
10629            panic!("Expected distance constraint");
10630        };
10631        assert_eq!(distance.label_position, Some(label_position));
10632
10633        mock_ctx.close().await;
10634    }
10635
10636    #[tokio::test(flavor = "multi_thread")]
10637    async fn test_edit_distance_constraint_label_position() {
10638        let initial_source = "\
10639sketch(on = XY) {
10640  point(at = [var 1, var 2])
10641  point(at = [var 3, var 2])
10642}
10643";
10644
10645        let program = Program::parse(initial_source).unwrap().0.unwrap();
10646
10647        let mut frontend = FrontendState::new();
10648
10649        let mock_ctx = ExecutorContext::new_mock(None).await;
10650        let version = Version(0);
10651
10652        frontend.program = program.clone();
10653        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10654        frontend.update_state_after_exec(outcome, true);
10655        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10656        let sketch_id = sketch_object.id;
10657        let sketch = expect_sketch(sketch_object);
10658        let point0_id = *sketch.segments.first().unwrap();
10659        let point1_id = *sketch.segments.get(1).unwrap();
10660
10661        let constraint = Constraint::Distance(Distance {
10662            points: vec![point0_id.into(), point1_id.into()],
10663            distance: Number {
10664                value: 2.0,
10665                units: NumericSuffix::Mm,
10666            },
10667            label_position: None,
10668            source: Default::default(),
10669        });
10670        let (_, scene_delta) = frontend
10671            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10672            .await
10673            .unwrap();
10674        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10675        let sketch = expect_sketch(sketch_object);
10676        let constraint_id = sketch.constraints[0];
10677        let label_position = Point2d {
10678            x: Number {
10679                value: 10.0,
10680                units: NumericSuffix::Mm,
10681            },
10682            y: Number {
10683                value: 11.0,
10684                units: NumericSuffix::Mm,
10685            },
10686        };
10687
10688        let (src_delta, scene_delta) = frontend
10689            .edit_distance_constraint_label_position(
10690                &mock_ctx,
10691                version,
10692                sketch_id,
10693                constraint_id,
10694                label_position.clone(),
10695                vec![],
10696            )
10697            .await
10698            .unwrap();
10699        assert_eq!(
10700            src_delta.text.as_str(),
10701            "\
10702sketch(on = XY) {
10703  point1 = point(at = [var 1, var 2])
10704  point2 = point(at = [var 3, var 2])
10705  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10706}
10707"
10708        );
10709
10710        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10711        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10712            panic!("Expected constraint object");
10713        };
10714        let Constraint::Distance(distance) = constraint else {
10715            panic!("Expected distance constraint");
10716        };
10717        assert_eq!(distance.label_position, Some(label_position));
10718
10719        mock_ctx.close().await;
10720    }
10721
10722    #[tokio::test(flavor = "multi_thread")]
10723    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10724        let initial_source = "\
10725sketch(on = XY) {
10726  point1 = point(at = [var 0mm, var 0mm])
10727  point2 = point(at = [var 10mm, var 0mm])
10728  distance([point1, point2]) == 5mm
10729}
10730";
10731
10732        let program = Program::parse(initial_source).unwrap().0.unwrap();
10733        let mut frontend = FrontendState::new();
10734        let mock_ctx = ExecutorContext::new_mock(None).await;
10735        let version = Version(0);
10736
10737        frontend.program = program.clone();
10738        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10739        frontend.update_state_after_exec(outcome, true);
10740        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10741        let sketch_id = sketch_object.id;
10742        let sketch = expect_sketch(sketch_object);
10743        let point0_id = sketch.segments[0];
10744        let point1_id = sketch.segments[1];
10745        let constraint_id = sketch.constraints[0];
10746
10747        let edited_segments = vec![ExistingSegmentCtor {
10748            id: point0_id,
10749            ctor: SegmentCtor::Point(PointCtor {
10750                position: Point2d {
10751                    x: Expr::Var(Number {
10752                        value: 2.0,
10753                        units: NumericSuffix::Mm,
10754                    }),
10755                    y: Expr::Var(Number {
10756                        value: 1.0,
10757                        units: NumericSuffix::Mm,
10758                    }),
10759                },
10760            }),
10761        }];
10762        let (_, scene_delta) = frontend
10763            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10764            .await
10765            .unwrap();
10766        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10767        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10768
10769        let label_position = Point2d {
10770            x: Number {
10771                value: 3.0,
10772                units: NumericSuffix::Mm,
10773            },
10774            y: Number {
10775                value: 4.0,
10776                units: NumericSuffix::Mm,
10777            },
10778        };
10779        let (_, scene_delta) = frontend
10780            .edit_distance_constraint_label_position(
10781                &mock_ctx,
10782                version,
10783                sketch_id,
10784                constraint_id,
10785                label_position,
10786                vec![point0_id],
10787            )
10788            .await
10789            .unwrap();
10790
10791        assert_point_position_close(
10792            point_position(&scene_delta.new_graph, point0_id),
10793            point0_after_segment_edit,
10794        );
10795        assert_point_position_close(
10796            point_position(&scene_delta.new_graph, point1_id),
10797            point1_after_segment_edit,
10798        );
10799
10800        mock_ctx.close().await;
10801    }
10802
10803    #[tokio::test(flavor = "multi_thread")]
10804    async fn test_distance_point_line() {
10805        let initial_source = "\
10806sketch(on = XY) {
10807  point(at = [var 0, var 5])
10808  line(start = [var 0, var 0], end = [var 10, var 0])
10809}
10810";
10811
10812        let program = Program::parse(initial_source).unwrap().0.unwrap();
10813
10814        let mut frontend = FrontendState::new();
10815
10816        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10817        let mock_ctx = ExecutorContext::new_mock(None).await;
10818        let version = Version(0);
10819
10820        frontend.hack_set_program(&ctx, program).await.unwrap();
10821        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10822        let sketch_id = sketch_object.id;
10823        let sketch = expect_sketch(sketch_object);
10824        let point_id = *sketch.segments.first().unwrap();
10825        let line_id = *sketch
10826            .segments
10827            .iter()
10828            .find(|segment_id| {
10829                matches!(
10830                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10831                    Some(ObjectKind::Segment {
10832                        segment: Segment::Line(_)
10833                    })
10834                )
10835            })
10836            .unwrap();
10837
10838        let label_position = Point2d {
10839            x: Number {
10840                value: 10.0,
10841                units: NumericSuffix::Mm,
10842            },
10843            y: Number {
10844                value: 11.0,
10845                units: NumericSuffix::Mm,
10846            },
10847        };
10848        let constraint = Constraint::Distance(Distance {
10849            points: vec![point_id.into(), line_id.into()],
10850            distance: Number {
10851                value: 5.0,
10852                units: NumericSuffix::Mm,
10853            },
10854            label_position: Some(label_position.clone()),
10855            source: Default::default(),
10856        });
10857        let (src_delta, scene_delta) = frontend
10858            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10859            .await
10860            .unwrap();
10861        assert_eq!(
10862            src_delta.text.as_str(),
10863            "\
10864sketch(on = XY) {
10865  point1 = point(at = [var 0, var 5])
10866  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10867  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
10868}
10869"
10870        );
10871        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10872        let sketch = expect_sketch(sketch_object);
10873        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10874        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10875            panic!("Expected constraint object");
10876        };
10877        let Constraint::Distance(distance) = constraint else {
10878            panic!("Expected distance constraint");
10879        };
10880        assert_eq!(distance.label_position, Some(label_position));
10881
10882        ctx.close().await;
10883        mock_ctx.close().await;
10884    }
10885
10886    #[tokio::test(flavor = "multi_thread")]
10887    async fn test_distance_point_arc() {
10888        let initial_source = "\
10889sketch(on = XY) {
10890  point(at = [var 0, var 8])
10891  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10892}
10893";
10894
10895        let program = Program::parse(initial_source).unwrap().0.unwrap();
10896
10897        let mut frontend = FrontendState::new();
10898
10899        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10900        let mock_ctx = ExecutorContext::new_mock(None).await;
10901        let version = Version(0);
10902
10903        frontend.hack_set_program(&ctx, program).await.unwrap();
10904        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10905        let sketch_id = sketch_object.id;
10906        let sketch = expect_sketch(sketch_object);
10907        let point_id = *sketch.segments.first().unwrap();
10908        let arc_id = *sketch
10909            .segments
10910            .iter()
10911            .find(|segment_id| {
10912                matches!(
10913                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10914                    Some(ObjectKind::Segment {
10915                        segment: Segment::Arc(_)
10916                    })
10917                )
10918            })
10919            .unwrap();
10920
10921        let constraint = Constraint::Distance(Distance {
10922            points: vec![point_id.into(), arc_id.into()],
10923            distance: Number {
10924                value: 3.0,
10925                units: NumericSuffix::Mm,
10926            },
10927            label_position: None,
10928            source: Default::default(),
10929        });
10930        let (src_delta, _scene_delta) = frontend
10931            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10932            .await
10933            .unwrap();
10934        assert_eq!(
10935            src_delta.text.as_str(),
10936            "\
10937sketch(on = XY) {
10938  point1 = point(at = [var 0, var 8])
10939  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10940  distance([point1, arc1]) == 3mm
10941}
10942"
10943        );
10944
10945        ctx.close().await;
10946        mock_ctx.close().await;
10947    }
10948
10949    #[tokio::test(flavor = "multi_thread")]
10950    async fn test_distance_arc_origin() {
10951        let initial_source = "\
10952sketch001 = sketch(on = XY) {
10953  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10954}
10955";
10956
10957        let program = Program::parse(initial_source).unwrap().0.unwrap();
10958
10959        let mut frontend = FrontendState::new();
10960
10961        let mock_ctx = ExecutorContext::new_mock(None).await;
10962        let version = Version(0);
10963
10964        frontend.program = program.clone();
10965        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10966        frontend.update_state_after_exec(outcome, true);
10967        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10968        let sketch_id = sketch_object.id;
10969        let sketch = expect_sketch(sketch_object);
10970        let arc_id = *sketch
10971            .segments
10972            .iter()
10973            .find(|segment_id| {
10974                matches!(
10975                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10976                    Some(ObjectKind::Segment {
10977                        segment: Segment::Arc(_)
10978                    })
10979                )
10980            })
10981            .unwrap();
10982
10983        let constraint = Constraint::Distance(Distance {
10984            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10985            distance: Number {
10986                value: 3.0,
10987                units: NumericSuffix::Mm,
10988            },
10989            label_position: None,
10990            source: Default::default(),
10991        });
10992        let (src_delta, _scene_delta) = frontend
10993            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10994            .await
10995            .unwrap();
10996        assert_eq!(
10997            src_delta.text.as_str(),
10998            "\
10999sketch001 = sketch(on = XY) {
11000  arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11001  distance([arc1, ORIGIN]) == 3mm
11002}
11003"
11004        );
11005
11006        mock_ctx.close().await;
11007    }
11008
11009    #[tokio::test(flavor = "multi_thread")]
11010    async fn test_distance_line_origin() {
11011        let initial_source = "\
11012sketch(on = XY) {
11013  line(start = [var 5, var 0], end = [var 5, var 10])
11014}
11015";
11016
11017        let program = Program::parse(initial_source).unwrap().0.unwrap();
11018
11019        let mut frontend = FrontendState::new();
11020
11021        let mock_ctx = ExecutorContext::new_mock(None).await;
11022        let version = Version(0);
11023
11024        frontend.program = program.clone();
11025        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11026        frontend.update_state_after_exec(outcome, true);
11027        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11028        let sketch_id = sketch_object.id;
11029        let sketch = expect_sketch(sketch_object);
11030        let line_id = *sketch
11031            .segments
11032            .iter()
11033            .find(|segment_id| {
11034                matches!(
11035                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11036                    Some(ObjectKind::Segment {
11037                        segment: Segment::Line(_)
11038                    })
11039                )
11040            })
11041            .unwrap();
11042
11043        let constraint = Constraint::Distance(Distance {
11044            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11045            distance: Number {
11046                value: 5.0,
11047                units: NumericSuffix::Mm,
11048            },
11049            label_position: None,
11050            source: Default::default(),
11051        });
11052        let (src_delta, _scene_delta) = frontend
11053            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11054            .await
11055            .unwrap();
11056        assert_eq!(
11057            src_delta.text.as_str(),
11058            "\
11059sketch(on = XY) {
11060  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11061  distance([ORIGIN, line1]) == 5mm
11062}
11063"
11064        );
11065
11066        mock_ctx.close().await;
11067    }
11068
11069    #[tokio::test(flavor = "multi_thread")]
11070    async fn test_distance_line_circle() {
11071        let initial_source = "\
11072sketch(on = XY) {
11073  line(start = [var -10, var 8], end = [var 10, var 8])
11074  circle(start = [var 5, var 0], center = [var 0, var 0])
11075}
11076";
11077
11078        let program = Program::parse(initial_source).unwrap().0.unwrap();
11079
11080        let mut frontend = FrontendState::new();
11081
11082        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11083        let mock_ctx = ExecutorContext::new_mock(None).await;
11084        let version = Version(0);
11085
11086        frontend.hack_set_program(&ctx, program).await.unwrap();
11087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11088        let sketch_id = sketch_object.id;
11089        let sketch = expect_sketch(sketch_object);
11090        let line_id = *sketch
11091            .segments
11092            .iter()
11093            .find(|segment_id| {
11094                matches!(
11095                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11096                    Some(ObjectKind::Segment {
11097                        segment: Segment::Line(_)
11098                    })
11099                )
11100            })
11101            .unwrap();
11102        let circle_id = *sketch
11103            .segments
11104            .iter()
11105            .find(|segment_id| {
11106                matches!(
11107                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11108                    Some(ObjectKind::Segment {
11109                        segment: Segment::Circle(_)
11110                    })
11111                )
11112            })
11113            .unwrap();
11114
11115        let constraint = Constraint::Distance(Distance {
11116            points: vec![line_id.into(), circle_id.into()],
11117            distance: Number {
11118                value: 3.0,
11119                units: NumericSuffix::Mm,
11120            },
11121            label_position: None,
11122            source: Default::default(),
11123        });
11124        let (src_delta, _scene_delta) = frontend
11125            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11126            .await
11127            .unwrap();
11128        assert_eq!(
11129            src_delta.text.as_str(),
11130            "\
11131sketch(on = XY) {
11132  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11133  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11134  distance([line1, circle1]) == 3mm
11135}
11136"
11137        );
11138
11139        ctx.close().await;
11140        mock_ctx.close().await;
11141    }
11142
11143    #[tokio::test(flavor = "multi_thread")]
11144    async fn test_distance_circle_arc() {
11145        let initial_source = "\
11146sketch(on = XY) {
11147  circle(start = [var 5, var 0], center = [var 0, var 0])
11148  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11149}
11150";
11151
11152        let program = Program::parse(initial_source).unwrap().0.unwrap();
11153
11154        let mut frontend = FrontendState::new();
11155
11156        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11157        let mock_ctx = ExecutorContext::new_mock(None).await;
11158        let version = Version(0);
11159
11160        frontend.hack_set_program(&ctx, program).await.unwrap();
11161        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11162        let sketch_id = sketch_object.id;
11163        let sketch = expect_sketch(sketch_object);
11164        let circle_id = *sketch
11165            .segments
11166            .iter()
11167            .find(|segment_id| {
11168                matches!(
11169                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11170                    Some(ObjectKind::Segment {
11171                        segment: Segment::Circle(_)
11172                    })
11173                )
11174            })
11175            .unwrap();
11176        let arc_id = *sketch
11177            .segments
11178            .iter()
11179            .find(|segment_id| {
11180                matches!(
11181                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11182                    Some(ObjectKind::Segment {
11183                        segment: Segment::Arc(_)
11184                    })
11185                )
11186            })
11187            .unwrap();
11188
11189        let constraint = Constraint::Distance(Distance {
11190            points: vec![circle_id.into(), arc_id.into()],
11191            distance: Number {
11192                value: 3.0,
11193                units: NumericSuffix::Mm,
11194            },
11195            label_position: None,
11196            source: Default::default(),
11197        });
11198        let (src_delta, _scene_delta) = frontend
11199            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11200            .await
11201            .unwrap();
11202        assert_eq!(
11203            src_delta.text.as_str(),
11204            "\
11205sketch(on = XY) {
11206  circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11207  arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11208  distance([circle1, arc1]) == 3mm
11209}
11210"
11211        );
11212
11213        ctx.close().await;
11214        mock_ctx.close().await;
11215    }
11216
11217    #[tokio::test(flavor = "multi_thread")]
11218    async fn test_distance_parallel_lines() {
11219        let initial_source = "\
11220sketch(on = XY) {
11221  line(start = [var 0, var 0], end = [var 10, var 0])
11222  line(start = [var 0, var 5], end = [var 10, var 5])
11223}
11224";
11225
11226        let program = Program::parse(initial_source).unwrap().0.unwrap();
11227
11228        let mut frontend = FrontendState::new();
11229
11230        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11231        let mock_ctx = ExecutorContext::new_mock(None).await;
11232        let version = Version(0);
11233
11234        frontend.hack_set_program(&ctx, program).await.unwrap();
11235        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11236        let sketch_id = sketch_object.id;
11237        let sketch = expect_sketch(sketch_object);
11238        let line_ids = sketch
11239            .segments
11240            .iter()
11241            .copied()
11242            .filter(|segment_id| {
11243                matches!(
11244                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11245                    Some(ObjectKind::Segment {
11246                        segment: Segment::Line(_)
11247                    })
11248                )
11249            })
11250            .collect::<Vec<_>>();
11251
11252        let constraint = Constraint::Distance(Distance {
11253            points: vec![line_ids[0].into(), line_ids[1].into()],
11254            distance: Number {
11255                value: 5.0,
11256                units: NumericSuffix::Mm,
11257            },
11258            label_position: None,
11259            source: Default::default(),
11260        });
11261        let (src_delta, _scene_delta) = frontend
11262            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11263            .await
11264            .unwrap();
11265        assert_eq!(
11266            src_delta.text.as_str(),
11267            "\
11268sketch(on = XY) {
11269  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11270  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11271  distance([line1, line2]) == 5mm
11272}
11273"
11274        );
11275
11276        ctx.close().await;
11277        mock_ctx.close().await;
11278    }
11279
11280    #[tokio::test(flavor = "multi_thread")]
11281    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11282        let initial_source = "\
11283sketch(on = XY) {
11284  line(start = [var 0, var 0], end = [var 10, var 0])
11285  line(start = [var 0, var 0], end = [var 0, var 10])
11286}
11287";
11288
11289        let program = Program::parse(initial_source).unwrap().0.unwrap();
11290
11291        let mut frontend = FrontendState::new();
11292
11293        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11294        let mock_ctx = ExecutorContext::new_mock(None).await;
11295        let version = Version(0);
11296
11297        frontend.hack_set_program(&ctx, program).await.unwrap();
11298        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11299        let sketch_id = sketch_object.id;
11300        let sketch = expect_sketch(sketch_object);
11301        let line_ids = sketch
11302            .segments
11303            .iter()
11304            .copied()
11305            .filter(|segment_id| {
11306                matches!(
11307                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11308                    Some(ObjectKind::Segment {
11309                        segment: Segment::Line(_)
11310                    })
11311                )
11312            })
11313            .collect::<Vec<_>>();
11314
11315        let constraint = Constraint::Distance(Distance {
11316            points: vec![line_ids[0].into(), line_ids[1].into()],
11317            distance: Number {
11318                value: 5.0,
11319                units: NumericSuffix::Mm,
11320            },
11321            label_position: None,
11322            source: Default::default(),
11323        });
11324        let (src_delta, _scene_delta) = frontend
11325            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11326            .await
11327            .unwrap();
11328        assert_eq!(
11329            src_delta.text.as_str(),
11330            "\
11331sketch(on = XY) {
11332  line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11333  line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11334  distance([line1, line2]) == 5mm
11335}
11336"
11337        );
11338
11339        ctx.close().await;
11340        mock_ctx.close().await;
11341    }
11342
11343    #[tokio::test(flavor = "multi_thread")]
11344    async fn test_horizontal_distance_two_points() {
11345        let initial_source = "\
11346sketch(on = XY) {
11347  point(at = [var 1, var 2])
11348  point(at = [var 3, var 4])
11349}
11350";
11351
11352        let program = Program::parse(initial_source).unwrap().0.unwrap();
11353
11354        let mut frontend = FrontendState::new();
11355
11356        let mock_ctx = ExecutorContext::new_mock(None).await;
11357        let version = Version(0);
11358
11359        frontend.program = program.clone();
11360        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11361        frontend.update_state_after_exec(outcome, true);
11362        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11363        let sketch_id = sketch_object.id;
11364        let sketch = expect_sketch(sketch_object);
11365        let point0_id = *sketch.segments.first().unwrap();
11366        let point1_id = *sketch.segments.get(1).unwrap();
11367        let label_position = Point2d {
11368            x: Number {
11369                value: 10.0,
11370                units: NumericSuffix::Mm,
11371            },
11372            y: Number {
11373                value: 11.0,
11374                units: NumericSuffix::Mm,
11375            },
11376        };
11377
11378        let constraint = Constraint::HorizontalDistance(Distance {
11379            points: vec![point0_id.into(), point1_id.into()],
11380            distance: Number {
11381                value: 2.0,
11382                units: NumericSuffix::Mm,
11383            },
11384            label_position: Some(label_position.clone()),
11385            source: Default::default(),
11386        });
11387        let (src_delta, scene_delta) = frontend
11388            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11389            .await
11390            .unwrap();
11391        assert_eq!(
11392            src_delta.text.as_str(),
11393            // The lack indentation is a formatter bug.
11394            "\
11395sketch(on = XY) {
11396  point1 = point(at = [var 1, var 2])
11397  point2 = point(at = [var 3, var 4])
11398  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11399}
11400"
11401        );
11402        assert_eq!(
11403            scene_delta.new_graph.objects.len(),
11404            5,
11405            "{:#?}",
11406            scene_delta.new_graph.objects
11407        );
11408        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11409        let sketch = expect_sketch(sketch_object);
11410        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11411        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11412            panic!("Expected constraint object");
11413        };
11414        let Constraint::HorizontalDistance(distance) = constraint else {
11415            panic!("Expected horizontal distance constraint");
11416        };
11417        assert_eq!(distance.label_position, Some(label_position));
11418
11419        mock_ctx.close().await;
11420    }
11421
11422    #[tokio::test(flavor = "multi_thread")]
11423    async fn test_radius_single_arc_segment() {
11424        let initial_source = "\
11425sketch(on = XY) {
11426  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11427}
11428";
11429
11430        let program = Program::parse(initial_source).unwrap().0.unwrap();
11431
11432        let mut frontend = FrontendState::new();
11433
11434        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11435        let mock_ctx = ExecutorContext::new_mock(None).await;
11436        let version = Version(0);
11437
11438        frontend.hack_set_program(&ctx, program).await.unwrap();
11439        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11440        let sketch_id = sketch_object.id;
11441        let sketch = expect_sketch(sketch_object);
11442        // Find the arc segment (not the points)
11443        let arc_id = sketch
11444            .segments
11445            .iter()
11446            .find(|&seg_id| {
11447                let obj = frontend.scene_graph.objects.get(seg_id.0);
11448                matches!(
11449                    obj.map(|o| &o.kind),
11450                    Some(ObjectKind::Segment {
11451                        segment: Segment::Arc(_)
11452                    })
11453                )
11454            })
11455            .unwrap();
11456
11457        let constraint = Constraint::Radius(Radius {
11458            arc: *arc_id,
11459            radius: Number {
11460                value: 5.0,
11461                units: NumericSuffix::Mm,
11462            },
11463            label_position: None,
11464            source: Default::default(),
11465        });
11466        let (src_delta, scene_delta) = frontend
11467            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11468            .await
11469            .unwrap();
11470        assert_eq!(
11471            src_delta.text.as_str(),
11472            // The lack indentation is a formatter bug.
11473            "\
11474sketch(on = XY) {
11475  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11476  radius(arc1) == 5mm
11477}
11478"
11479        );
11480        assert_eq!(
11481            scene_delta.new_graph.objects.len(),
11482            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11483            "{:#?}",
11484            scene_delta.new_graph.objects
11485        );
11486
11487        ctx.close().await;
11488        mock_ctx.close().await;
11489    }
11490
11491    #[tokio::test(flavor = "multi_thread")]
11492    async fn test_radius_single_arc_segment_with_label_position() {
11493        let initial_source = "\
11494sketch(on = XY) {
11495  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11496}
11497";
11498
11499        let program = Program::parse(initial_source).unwrap().0.unwrap();
11500        let mut frontend = FrontendState::new();
11501        let mock_ctx = ExecutorContext::new_mock(None).await;
11502        let version = Version(0);
11503
11504        frontend.program = program.clone();
11505        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11506        frontend.update_state_after_exec(outcome, true);
11507        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11508        let sketch_id = sketch_object.id;
11509        let sketch = expect_sketch(sketch_object);
11510        let arc_id = sketch
11511            .segments
11512            .iter()
11513            .find(|&seg_id| {
11514                let obj = frontend.scene_graph.objects.get(seg_id.0);
11515                matches!(
11516                    obj.map(|o| &o.kind),
11517                    Some(ObjectKind::Segment {
11518                        segment: Segment::Arc(_)
11519                    })
11520                )
11521            })
11522            .unwrap();
11523
11524        let label_position = Point2d {
11525            x: Number {
11526                value: 10.0,
11527                units: NumericSuffix::Mm,
11528            },
11529            y: Number {
11530                value: 11.0,
11531                units: NumericSuffix::Mm,
11532            },
11533        };
11534        let constraint = Constraint::Radius(Radius {
11535            arc: *arc_id,
11536            radius: Number {
11537                value: 5.0,
11538                units: NumericSuffix::Mm,
11539            },
11540            label_position: Some(label_position.clone()),
11541            source: Default::default(),
11542        });
11543        let (src_delta, scene_delta) = frontend
11544            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11545            .await
11546            .unwrap();
11547        assert_eq!(
11548            src_delta.text.as_str(),
11549            "\
11550sketch(on = XY) {
11551  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11552  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11553}
11554"
11555        );
11556
11557        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11558        let sketch = expect_sketch(sketch_object);
11559        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11560        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11561            panic!("Expected constraint object");
11562        };
11563        let Constraint::Radius(radius) = constraint else {
11564            panic!("Expected radius constraint");
11565        };
11566        assert_eq!(radius.label_position, Some(label_position));
11567
11568        mock_ctx.close().await;
11569    }
11570
11571    #[tokio::test(flavor = "multi_thread")]
11572    async fn test_edit_radius_constraint_label_position() {
11573        let initial_source = "\
11574sketch(on = XY) {
11575  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11576  radius(arc1) == 5mm
11577}
11578";
11579
11580        let program = Program::parse(initial_source).unwrap().0.unwrap();
11581        let mut frontend = FrontendState::new();
11582        let mock_ctx = ExecutorContext::new_mock(None).await;
11583        let version = Version(0);
11584
11585        frontend.program = program.clone();
11586        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11587        frontend.update_state_after_exec(outcome, true);
11588        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11589        let sketch_id = sketch_object.id;
11590        let sketch = expect_sketch(sketch_object);
11591        let constraint_id = sketch.constraints[0];
11592        let label_position = Point2d {
11593            x: Number {
11594                value: 10.0,
11595                units: NumericSuffix::Mm,
11596            },
11597            y: Number {
11598                value: 11.0,
11599                units: NumericSuffix::Mm,
11600            },
11601        };
11602
11603        let (src_delta, scene_delta) = frontend
11604            .edit_distance_constraint_label_position(
11605                &mock_ctx,
11606                version,
11607                sketch_id,
11608                constraint_id,
11609                label_position.clone(),
11610                vec![],
11611            )
11612            .await
11613            .unwrap();
11614        assert_eq!(
11615            src_delta.text.as_str(),
11616            "\
11617sketch(on = XY) {
11618  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11619  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11620}
11621"
11622        );
11623
11624        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11625        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11626            panic!("Expected constraint object");
11627        };
11628        let Constraint::Radius(radius) = constraint else {
11629            panic!("Expected radius constraint");
11630        };
11631        assert_eq!(radius.label_position, Some(label_position));
11632
11633        mock_ctx.close().await;
11634    }
11635
11636    #[tokio::test(flavor = "multi_thread")]
11637    async fn test_vertical_distance_two_points() {
11638        let initial_source = "\
11639sketch(on = XY) {
11640  point(at = [var 1, var 2])
11641  point(at = [var 3, var 4])
11642}
11643";
11644
11645        let program = Program::parse(initial_source).unwrap().0.unwrap();
11646
11647        let mut frontend = FrontendState::new();
11648
11649        let mock_ctx = ExecutorContext::new_mock(None).await;
11650        let version = Version(0);
11651
11652        frontend.program = program.clone();
11653        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11654        frontend.update_state_after_exec(outcome, true);
11655        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11656        let sketch_id = sketch_object.id;
11657        let sketch = expect_sketch(sketch_object);
11658        let point0_id = *sketch.segments.first().unwrap();
11659        let point1_id = *sketch.segments.get(1).unwrap();
11660        let label_position = Point2d {
11661            x: Number {
11662                value: 10.0,
11663                units: NumericSuffix::Mm,
11664            },
11665            y: Number {
11666                value: 11.0,
11667                units: NumericSuffix::Mm,
11668            },
11669        };
11670
11671        let constraint = Constraint::VerticalDistance(Distance {
11672            points: vec![point0_id.into(), point1_id.into()],
11673            distance: Number {
11674                value: 2.0,
11675                units: NumericSuffix::Mm,
11676            },
11677            label_position: Some(label_position.clone()),
11678            source: Default::default(),
11679        });
11680        let (src_delta, scene_delta) = frontend
11681            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11682            .await
11683            .unwrap();
11684        assert_eq!(
11685            src_delta.text.as_str(),
11686            // The lack indentation is a formatter bug.
11687            "\
11688sketch(on = XY) {
11689  point1 = point(at = [var 1, var 2])
11690  point2 = point(at = [var 3, var 4])
11691  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11692}
11693"
11694        );
11695        assert_eq!(
11696            scene_delta.new_graph.objects.len(),
11697            5,
11698            "{:#?}",
11699            scene_delta.new_graph.objects
11700        );
11701        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11702        let sketch = expect_sketch(sketch_object);
11703        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11704        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11705            panic!("Expected constraint object");
11706        };
11707        let Constraint::VerticalDistance(distance) = constraint else {
11708            panic!("Expected vertical distance constraint");
11709        };
11710        assert_eq!(distance.label_position, Some(label_position));
11711
11712        mock_ctx.close().await;
11713    }
11714
11715    #[tokio::test(flavor = "multi_thread")]
11716    async fn test_add_fixed_standalone_point() {
11717        let initial_source = "\
11718sketch(on = XY) {
11719  point(at = [var 1, var 2])
11720}
11721";
11722
11723        let program = Program::parse(initial_source).unwrap().0.unwrap();
11724
11725        let mut frontend = FrontendState::new();
11726
11727        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11728        let mock_ctx = ExecutorContext::new_mock(None).await;
11729        let version = Version(0);
11730
11731        frontend.hack_set_program(&ctx, program).await.unwrap();
11732        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11733        let sketch_id = sketch_object.id;
11734        let sketch = expect_sketch(sketch_object);
11735        let point_id = *sketch.segments.first().unwrap();
11736
11737        let (src_delta, scene_delta) = frontend
11738            .add_constraint(
11739                &mock_ctx,
11740                version,
11741                sketch_id,
11742                Constraint::Fixed(Fixed {
11743                    points: vec![FixedPoint {
11744                        point: point_id,
11745                        position: Point2d {
11746                            x: Number {
11747                                value: 2.0,
11748                                units: NumericSuffix::Mm,
11749                            },
11750                            y: Number {
11751                                value: 3.0,
11752                                units: NumericSuffix::Mm,
11753                            },
11754                        },
11755                    }],
11756                }),
11757            )
11758            .await
11759            .unwrap();
11760        assert_eq!(
11761            src_delta.text.as_str(),
11762            "\
11763sketch(on = XY) {
11764  point1 = point(at = [var 2, var 3])
11765  fixed([point1, [2mm, 3mm]])
11766}
11767"
11768        );
11769        assert_eq!(
11770            scene_delta.new_graph.objects.len(),
11771            4,
11772            "{:#?}",
11773            scene_delta.new_graph.objects
11774        );
11775
11776        ctx.close().await;
11777        mock_ctx.close().await;
11778    }
11779
11780    #[tokio::test(flavor = "multi_thread")]
11781    async fn test_add_fixed_multiple_points() {
11782        let initial_source = "\
11783sketch(on = XY) {
11784  point(at = [var 1, var 2])
11785  point(at = [var 3, var 4])
11786}
11787";
11788
11789        let program = Program::parse(initial_source).unwrap().0.unwrap();
11790
11791        let mut frontend = FrontendState::new();
11792
11793        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11794        let mock_ctx = ExecutorContext::new_mock(None).await;
11795        let version = Version(0);
11796
11797        frontend.hack_set_program(&ctx, program).await.unwrap();
11798        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11799        let sketch_id = sketch_object.id;
11800        let sketch = expect_sketch(sketch_object);
11801        let point0_id = *sketch.segments.first().unwrap();
11802        let point1_id = *sketch.segments.get(1).unwrap();
11803
11804        let (src_delta, scene_delta) = frontend
11805            .add_constraint(
11806                &mock_ctx,
11807                version,
11808                sketch_id,
11809                Constraint::Fixed(Fixed {
11810                    points: vec![
11811                        FixedPoint {
11812                            point: point0_id,
11813                            position: Point2d {
11814                                x: Number {
11815                                    value: 2.0,
11816                                    units: NumericSuffix::Mm,
11817                                },
11818                                y: Number {
11819                                    value: 3.0,
11820                                    units: NumericSuffix::Mm,
11821                                },
11822                            },
11823                        },
11824                        FixedPoint {
11825                            point: point1_id,
11826                            position: Point2d {
11827                                x: Number {
11828                                    value: 4.0,
11829                                    units: NumericSuffix::Mm,
11830                                },
11831                                y: Number {
11832                                    value: 5.0,
11833                                    units: NumericSuffix::Mm,
11834                                },
11835                            },
11836                        },
11837                    ],
11838                }),
11839            )
11840            .await
11841            .unwrap();
11842        assert_eq!(
11843            src_delta.text.as_str(),
11844            "\
11845sketch(on = XY) {
11846  point1 = point(at = [var 2, var 3])
11847  point2 = point(at = [var 4, var 5])
11848  fixed([point1, [2mm, 3mm]])
11849  fixed([point2, [4mm, 5mm]])
11850}
11851"
11852        );
11853        assert_eq!(
11854            scene_delta.new_graph.objects.len(),
11855            6,
11856            "{:#?}",
11857            scene_delta.new_graph.objects
11858        );
11859
11860        ctx.close().await;
11861        mock_ctx.close().await;
11862    }
11863
11864    #[tokio::test(flavor = "multi_thread")]
11865    async fn test_add_fixed_owned_point() {
11866        let initial_source = "\
11867sketch(on = XY) {
11868  line(start = [var 1, var 2], end = [var 3, var 4])
11869}
11870";
11871
11872        let program = Program::parse(initial_source).unwrap().0.unwrap();
11873
11874        let mut frontend = FrontendState::new();
11875
11876        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11877        let mock_ctx = ExecutorContext::new_mock(None).await;
11878        let version = Version(0);
11879
11880        frontend.hack_set_program(&ctx, program).await.unwrap();
11881        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11882        let sketch_id = sketch_object.id;
11883        let sketch = expect_sketch(sketch_object);
11884        let line_start_id = *sketch.segments.first().unwrap();
11885
11886        let (src_delta, scene_delta) = frontend
11887            .add_constraint(
11888                &mock_ctx,
11889                version,
11890                sketch_id,
11891                Constraint::Fixed(Fixed {
11892                    points: vec![FixedPoint {
11893                        point: line_start_id,
11894                        position: Point2d {
11895                            x: Number {
11896                                value: 2.0,
11897                                units: NumericSuffix::Mm,
11898                            },
11899                            y: Number {
11900                                value: 3.0,
11901                                units: NumericSuffix::Mm,
11902                            },
11903                        },
11904                    }],
11905                }),
11906            )
11907            .await
11908            .unwrap();
11909        assert_eq!(
11910            src_delta.text.as_str(),
11911            "\
11912sketch(on = XY) {
11913  line1 = line(start = [var 2, var 3], end = [var 3, var 4])
11914  fixed([line1.start, [2mm, 3mm]])
11915}
11916"
11917        );
11918        assert_eq!(
11919            scene_delta.new_graph.objects.len(),
11920            6,
11921            "{:#?}",
11922            scene_delta.new_graph.objects
11923        );
11924
11925        ctx.close().await;
11926        mock_ctx.close().await;
11927    }
11928
11929    #[tokio::test(flavor = "multi_thread")]
11930    async fn test_radius_error_cases() {
11931        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11932        let mock_ctx = ExecutorContext::new_mock(None).await;
11933        let version = Version(0);
11934
11935        // Test: Single point should error
11936        let initial_source_point = "\
11937sketch(on = XY) {
11938  point(at = [var 1, var 2])
11939}
11940";
11941        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11942        let mut frontend_point = FrontendState::new();
11943        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11944        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11945        let sketch_id_point = sketch_object_point.id;
11946        let sketch_point = expect_sketch(sketch_object_point);
11947        let point_id = *sketch_point.segments.first().unwrap();
11948
11949        let constraint_point = Constraint::Radius(Radius {
11950            arc: point_id,
11951            radius: Number {
11952                value: 5.0,
11953                units: NumericSuffix::Mm,
11954            },
11955            label_position: None,
11956            source: Default::default(),
11957        });
11958        let result_point = frontend_point
11959            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11960            .await;
11961        assert!(result_point.is_err(), "Single point should error for radius");
11962
11963        // Test: Single line segment should error (only arc segments supported)
11964        let initial_source_line = "\
11965sketch(on = XY) {
11966  line(start = [var 1, var 2], end = [var 3, var 4])
11967}
11968";
11969        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11970        let mut frontend_line = FrontendState::new();
11971        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11972        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11973        let sketch_id_line = sketch_object_line.id;
11974        let sketch_line = expect_sketch(sketch_object_line);
11975        let line_id = *sketch_line.segments.first().unwrap();
11976
11977        let constraint_line = Constraint::Radius(Radius {
11978            arc: line_id,
11979            radius: Number {
11980                value: 5.0,
11981                units: NumericSuffix::Mm,
11982            },
11983            label_position: None,
11984            source: Default::default(),
11985        });
11986        let result_line = frontend_line
11987            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11988            .await;
11989        assert!(result_line.is_err(), "Single line segment should error for radius");
11990
11991        ctx.close().await;
11992        mock_ctx.close().await;
11993    }
11994
11995    #[tokio::test(flavor = "multi_thread")]
11996    async fn test_diameter_single_arc_segment() {
11997        let initial_source = "\
11998sketch(on = XY) {
11999  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12000}
12001";
12002
12003        let program = Program::parse(initial_source).unwrap().0.unwrap();
12004
12005        let mut frontend = FrontendState::new();
12006
12007        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12008        let mock_ctx = ExecutorContext::new_mock(None).await;
12009        let version = Version(0);
12010
12011        frontend.hack_set_program(&ctx, program).await.unwrap();
12012        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12013        let sketch_id = sketch_object.id;
12014        let sketch = expect_sketch(sketch_object);
12015        // Find the arc segment (not the points)
12016        let arc_id = sketch
12017            .segments
12018            .iter()
12019            .find(|&seg_id| {
12020                let obj = frontend.scene_graph.objects.get(seg_id.0);
12021                matches!(
12022                    obj.map(|o| &o.kind),
12023                    Some(ObjectKind::Segment {
12024                        segment: Segment::Arc(_)
12025                    })
12026                )
12027            })
12028            .unwrap();
12029
12030        let constraint = Constraint::Diameter(Diameter {
12031            arc: *arc_id,
12032            diameter: Number {
12033                value: 10.0,
12034                units: NumericSuffix::Mm,
12035            },
12036            label_position: None,
12037            source: Default::default(),
12038        });
12039        let (src_delta, scene_delta) = frontend
12040            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12041            .await
12042            .unwrap();
12043        assert_eq!(
12044            src_delta.text.as_str(),
12045            // The lack indentation is a formatter bug.
12046            "\
12047sketch(on = XY) {
12048  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12049  diameter(arc1) == 10mm
12050}
12051"
12052        );
12053        assert_eq!(
12054            scene_delta.new_graph.objects.len(),
12055            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12056            "{:#?}",
12057            scene_delta.new_graph.objects
12058        );
12059
12060        ctx.close().await;
12061        mock_ctx.close().await;
12062    }
12063
12064    #[tokio::test(flavor = "multi_thread")]
12065    async fn test_diameter_single_arc_segment_with_label_position() {
12066        let initial_source = "\
12067sketch(on = XY) {
12068  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12069}
12070";
12071
12072        let program = Program::parse(initial_source).unwrap().0.unwrap();
12073        let mut frontend = FrontendState::new();
12074        let mock_ctx = ExecutorContext::new_mock(None).await;
12075        let version = Version(0);
12076
12077        frontend.program = program.clone();
12078        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12079        frontend.update_state_after_exec(outcome, true);
12080        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12081        let sketch_id = sketch_object.id;
12082        let sketch = expect_sketch(sketch_object);
12083        let arc_id = sketch
12084            .segments
12085            .iter()
12086            .find(|&seg_id| {
12087                let obj = frontend.scene_graph.objects.get(seg_id.0);
12088                matches!(
12089                    obj.map(|o| &o.kind),
12090                    Some(ObjectKind::Segment {
12091                        segment: Segment::Arc(_)
12092                    })
12093                )
12094            })
12095            .unwrap();
12096
12097        let label_position = Point2d {
12098            x: Number {
12099                value: 10.0,
12100                units: NumericSuffix::Mm,
12101            },
12102            y: Number {
12103                value: 11.0,
12104                units: NumericSuffix::Mm,
12105            },
12106        };
12107        let constraint = Constraint::Diameter(Diameter {
12108            arc: *arc_id,
12109            diameter: Number {
12110                value: 10.0,
12111                units: NumericSuffix::Mm,
12112            },
12113            label_position: Some(label_position.clone()),
12114            source: Default::default(),
12115        });
12116        let (src_delta, scene_delta) = frontend
12117            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12118            .await
12119            .unwrap();
12120        assert_eq!(
12121            src_delta.text.as_str(),
12122            "\
12123sketch(on = XY) {
12124  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12125  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12126}
12127"
12128        );
12129
12130        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12131        let sketch = expect_sketch(sketch_object);
12132        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12133        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12134            panic!("Expected constraint object");
12135        };
12136        let Constraint::Diameter(diameter) = constraint else {
12137            panic!("Expected diameter constraint");
12138        };
12139        assert_eq!(diameter.label_position, Some(label_position));
12140
12141        mock_ctx.close().await;
12142    }
12143
12144    #[tokio::test(flavor = "multi_thread")]
12145    async fn test_edit_diameter_constraint_label_position() {
12146        let initial_source = "\
12147sketch(on = XY) {
12148  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12149  diameter(arc1) == 10mm
12150}
12151";
12152
12153        let program = Program::parse(initial_source).unwrap().0.unwrap();
12154        let mut frontend = FrontendState::new();
12155        let mock_ctx = ExecutorContext::new_mock(None).await;
12156        let version = Version(0);
12157
12158        frontend.program = program.clone();
12159        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12160        frontend.update_state_after_exec(outcome, true);
12161        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12162        let sketch_id = sketch_object.id;
12163        let sketch = expect_sketch(sketch_object);
12164        let constraint_id = sketch.constraints[0];
12165        let label_position = Point2d {
12166            x: Number {
12167                value: 10.0,
12168                units: NumericSuffix::Mm,
12169            },
12170            y: Number {
12171                value: 11.0,
12172                units: NumericSuffix::Mm,
12173            },
12174        };
12175
12176        let (src_delta, scene_delta) = frontend
12177            .edit_distance_constraint_label_position(
12178                &mock_ctx,
12179                version,
12180                sketch_id,
12181                constraint_id,
12182                label_position.clone(),
12183                vec![],
12184            )
12185            .await
12186            .unwrap();
12187        assert_eq!(
12188            src_delta.text.as_str(),
12189            "\
12190sketch(on = XY) {
12191  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12192  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12193}
12194"
12195        );
12196
12197        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12198        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12199            panic!("Expected constraint object");
12200        };
12201        let Constraint::Diameter(diameter) = constraint else {
12202            panic!("Expected diameter constraint");
12203        };
12204        assert_eq!(diameter.label_position, Some(label_position));
12205
12206        mock_ctx.close().await;
12207    }
12208
12209    #[tokio::test(flavor = "multi_thread")]
12210    async fn test_diameter_error_cases() {
12211        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12212        let mock_ctx = ExecutorContext::new_mock(None).await;
12213        let version = Version(0);
12214
12215        // Test: Single point should error
12216        let initial_source_point = "\
12217sketch(on = XY) {
12218  point(at = [var 1, var 2])
12219}
12220";
12221        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12222        let mut frontend_point = FrontendState::new();
12223        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12224        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12225        let sketch_id_point = sketch_object_point.id;
12226        let sketch_point = expect_sketch(sketch_object_point);
12227        let point_id = *sketch_point.segments.first().unwrap();
12228
12229        let constraint_point = Constraint::Diameter(Diameter {
12230            arc: point_id,
12231            diameter: Number {
12232                value: 10.0,
12233                units: NumericSuffix::Mm,
12234            },
12235            label_position: None,
12236            source: Default::default(),
12237        });
12238        let result_point = frontend_point
12239            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12240            .await;
12241        assert!(result_point.is_err(), "Single point should error for diameter");
12242
12243        // Test: Single line segment should error (only arc segments supported)
12244        let initial_source_line = "\
12245sketch(on = XY) {
12246  line(start = [var 1, var 2], end = [var 3, var 4])
12247}
12248";
12249        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12250        let mut frontend_line = FrontendState::new();
12251        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12252        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12253        let sketch_id_line = sketch_object_line.id;
12254        let sketch_line = expect_sketch(sketch_object_line);
12255        let line_id = *sketch_line.segments.first().unwrap();
12256
12257        let constraint_line = Constraint::Diameter(Diameter {
12258            arc: line_id,
12259            diameter: Number {
12260                value: 10.0,
12261                units: NumericSuffix::Mm,
12262            },
12263            label_position: None,
12264            source: Default::default(),
12265        });
12266        let result_line = frontend_line
12267            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12268            .await;
12269        assert!(result_line.is_err(), "Single line segment should error for diameter");
12270
12271        ctx.close().await;
12272        mock_ctx.close().await;
12273    }
12274
12275    #[tokio::test(flavor = "multi_thread")]
12276    async fn test_line_horizontal() {
12277        let initial_source = "\
12278sketch(on = XY) {
12279  line(start = [var 1, var 2], end = [var 3, var 4])
12280}
12281";
12282
12283        let program = Program::parse(initial_source).unwrap().0.unwrap();
12284
12285        let mut frontend = FrontendState::new();
12286
12287        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12288        let mock_ctx = ExecutorContext::new_mock(None).await;
12289        let version = Version(0);
12290
12291        frontend.hack_set_program(&ctx, program).await.unwrap();
12292        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12293        let sketch_id = sketch_object.id;
12294        let sketch = expect_sketch(sketch_object);
12295        let line1_id = *sketch.segments.get(2).unwrap();
12296
12297        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12298        let (src_delta, scene_delta) = frontend
12299            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12300            .await
12301            .unwrap();
12302        assert_eq!(
12303            src_delta.text.as_str(),
12304            "\
12305sketch(on = XY) {
12306  line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12307  horizontal(line1)
12308}
12309"
12310        );
12311        assert_eq!(
12312            scene_delta.new_graph.objects.len(),
12313            6,
12314            "{:#?}",
12315            scene_delta.new_graph.objects
12316        );
12317
12318        ctx.close().await;
12319        mock_ctx.close().await;
12320    }
12321
12322    #[tokio::test(flavor = "multi_thread")]
12323    async fn test_control_point_spline_edge_horizontal() {
12324        let initial_source = "\
12325@settings(experimentalFeatures = allow)
12326splineSketch = sketch(on = XY) {
12327  controlPointSpline1 = controlPointSpline(points = [
12328    [var 0mm, var 0mm],
12329    [var 10mm, var 20mm],
12330    [var 20mm, var 0mm],
12331  ])
12332}
12333";
12334
12335        let program = Program::parse(initial_source).unwrap().0.unwrap();
12336
12337        let mut frontend = FrontendState::new();
12338
12339        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12340        let mock_ctx = ExecutorContext::new_mock(None).await;
12341        let version = Version(0);
12342
12343        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12344        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12345        let sketch_id = sketch_object.id;
12346        let sketch = expect_sketch(sketch_object);
12347        let spline_id = sketch
12348            .segments
12349            .iter()
12350            .copied()
12351            .find(|seg_id| {
12352                matches!(
12353                    &frontend.scene_graph.objects[seg_id.0].kind,
12354                    ObjectKind::Segment {
12355                        segment: Segment::ControlPointSpline(_)
12356                    }
12357                )
12358            })
12359            .expect("Expected a control point spline segment in sketch");
12360        let edge_id = frontend
12361            .scene_graph
12362            .objects
12363            .iter()
12364            .find_map(|obj| match &obj.kind {
12365                ObjectKind::Segment {
12366                    segment: Segment::Line(line),
12367                } if line.owner == Some(spline_id) => Some(obj.id),
12368                _ => None,
12369            })
12370            .expect("Expected an owned control-polygon edge");
12371
12372        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12373        let (src_delta, _) = frontend
12374            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12375            .await
12376            .unwrap();
12377        assert!(
12378            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12379            "Expected horizontal constraint on spline edge, got: {}",
12380            src_delta.text
12381        );
12382
12383        ctx.close().await;
12384        mock_ctx.close().await;
12385    }
12386
12387    #[tokio::test(flavor = "multi_thread")]
12388    async fn test_control_point_spline_edge_angle() {
12389        let initial_source = "\
12390@settings(experimentalFeatures = allow)
12391splineSketch = sketch(on = XY) {
12392  controlPointSpline1 = controlPointSpline(points = [
12393    [var 0mm, var 0mm],
12394    [var 10mm, var 20mm],
12395    [var 20mm, var 0mm],
12396  ])
12397
12398  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12399}
12400";
12401
12402        let program = Program::parse(initial_source).unwrap().0.unwrap();
12403
12404        let mut frontend = FrontendState::new();
12405
12406        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12407        let mock_ctx = ExecutorContext::new_mock(None).await;
12408        let version = Version(0);
12409
12410        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12411        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12412        let sketch_id = sketch_object.id;
12413        let sketch = expect_sketch(sketch_object);
12414        let spline_id = sketch
12415            .segments
12416            .iter()
12417            .copied()
12418            .find(|seg_id| {
12419                matches!(
12420                    &frontend.scene_graph.objects[seg_id.0].kind,
12421                    ObjectKind::Segment {
12422                        segment: Segment::ControlPointSpline(_)
12423                    }
12424                )
12425            })
12426            .expect("Expected a control point spline segment in sketch");
12427        let edge_id = frontend
12428            .scene_graph
12429            .objects
12430            .iter()
12431            .find_map(|obj| match &obj.kind {
12432                ObjectKind::Segment {
12433                    segment: Segment::Line(line),
12434                } if line.owner == Some(spline_id) => Some(obj.id),
12435                _ => None,
12436            })
12437            .expect("Expected an owned control-polygon edge");
12438        let line1_id = frontend
12439            .scene_graph
12440            .objects
12441            .iter()
12442            .find_map(|obj| match &obj.kind {
12443                ObjectKind::Segment {
12444                    segment: Segment::Line(line),
12445                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12446                _ => None,
12447            })
12448            .or_else(|| {
12449                sketch.segments.iter().copied().find(|seg_id| {
12450                    matches!(
12451                        &frontend.scene_graph.objects[seg_id.0].kind,
12452                        ObjectKind::Segment {
12453                            segment: Segment::Line(line),
12454                        } if line.owner.is_none()
12455                    )
12456                })
12457            })
12458            .expect("Expected a standalone line segment in sketch");
12459
12460        let constraint = Constraint::Angle(Angle {
12461            lines: vec![line1_id, edge_id],
12462            angle: Number {
12463                value: 30.0,
12464                units: NumericSuffix::Deg,
12465            },
12466            source: Default::default(),
12467        });
12468        let (src_delta, _) = frontend
12469            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12470            .await
12471            .unwrap();
12472        assert!(
12473            src_delta
12474                .text
12475                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12476            "Expected angle constraint on spline edge, got: {}",
12477            src_delta.text
12478        );
12479
12480        ctx.close().await;
12481        mock_ctx.close().await;
12482    }
12483
12484    #[tokio::test(flavor = "multi_thread")]
12485    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12486        let initial_source = "\
12487@settings(experimentalFeatures = allow)
12488splineSketch = sketch(on = XY) {
12489  spline1 = controlPointSpline(points = [
12490    [var 0mm, var 0mm],
12491    [var 10mm, var 20mm],
12492    [var 20mm, var 0mm],
12493  ])
12494  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12495  coincident([spline1.controls[1], spline1.edges[0]])
12496  coincident([spline1.controls[0], line1])
12497}
12498";
12499
12500        let program = Program::parse(initial_source).unwrap().0.unwrap();
12501
12502        let mut frontend = FrontendState::new();
12503
12504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12505        let mock_ctx = ExecutorContext::new_mock(None).await;
12506
12507        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12508
12509        let ui_scene_graph = frontend.scene_graph_for_ui();
12510        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12511        let sketch = expect_sketch(sketch_object);
12512
12513        assert_eq!(
12514            sketch.constraints.len(),
12515            1,
12516            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12517        );
12518
12519        let visible_constraints = ui_scene_graph
12520            .objects
12521            .iter()
12522            .filter_map(|object| match &object.kind {
12523                ObjectKind::Constraint {
12524                    constraint: Constraint::Coincident(coincident),
12525                } => Some(coincident.clone()),
12526                _ => None,
12527            })
12528            .collect::<Vec<_>>();
12529
12530        assert_eq!(
12531            visible_constraints.len(),
12532            1,
12533            "Expected only one coincident constraint object in the UI scene graph"
12534        );
12535        assert_eq!(
12536            visible_constraints[0].get_segments().len(),
12537            2,
12538            "Expected the remaining visible coincident constraint to reference two segments"
12539        );
12540
12541        ctx.close().await;
12542        mock_ctx.close().await;
12543    }
12544
12545    #[tokio::test(flavor = "multi_thread")]
12546    async fn test_edit_control_point_spline_can_append_control_point() {
12547        let initial_source = "\
12548@settings(experimentalFeatures = allow)
12549splineSketch = sketch(on = XY) {
12550  controlPointSpline(points = [
12551    [var 0mm, var 0mm],
12552    [var 10mm, var 20mm],
12553    [var 20mm, var 0mm],
12554  ])
12555}
12556";
12557
12558        let program = Program::parse(initial_source).unwrap().0.unwrap();
12559
12560        let mut frontend = FrontendState::new();
12561
12562        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12563        let mock_ctx = ExecutorContext::new_mock(None).await;
12564        let version = Version(0);
12565
12566        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12567        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12568        let sketch_id = sketch_object.id;
12569        let sketch = expect_sketch(sketch_object);
12570        let spline_id = sketch
12571            .segments
12572            .iter()
12573            .copied()
12574            .find(|seg_id| {
12575                matches!(
12576                    &frontend.scene_graph.objects[seg_id.0].kind,
12577                    ObjectKind::Segment {
12578                        segment: Segment::ControlPointSpline(_)
12579                    }
12580                )
12581            })
12582            .expect("Expected a control point spline segment in sketch");
12583
12584        let ctor = ControlPointSplineCtor {
12585            points: vec![
12586                Point2d {
12587                    x: Expr::Var(Number {
12588                        value: 0.0,
12589                        units: NumericSuffix::Mm,
12590                    }),
12591                    y: Expr::Var(Number {
12592                        value: 0.0,
12593                        units: NumericSuffix::Mm,
12594                    }),
12595                },
12596                Point2d {
12597                    x: Expr::Var(Number {
12598                        value: 10.0,
12599                        units: NumericSuffix::Mm,
12600                    }),
12601                    y: Expr::Var(Number {
12602                        value: 20.0,
12603                        units: NumericSuffix::Mm,
12604                    }),
12605                },
12606                Point2d {
12607                    x: Expr::Var(Number {
12608                        value: 20.0,
12609                        units: NumericSuffix::Mm,
12610                    }),
12611                    y: Expr::Var(Number {
12612                        value: 0.0,
12613                        units: NumericSuffix::Mm,
12614                    }),
12615                },
12616                Point2d {
12617                    x: Expr::Var(Number {
12618                        value: 30.0,
12619                        units: NumericSuffix::Mm,
12620                    }),
12621                    y: Expr::Var(Number {
12622                        value: 10.0,
12623                        units: NumericSuffix::Mm,
12624                    }),
12625                },
12626            ],
12627            construction: None,
12628        };
12629
12630        let segments = vec![ExistingSegmentCtor {
12631            id: spline_id,
12632            ctor: SegmentCtor::ControlPointSpline(ctor),
12633        }];
12634        let (src_delta, scene_delta) = frontend
12635            .edit_segments(&mock_ctx, version, sketch_id, segments)
12636            .await
12637            .unwrap();
12638
12639        assert!(
12640            src_delta.text.contains("[var 30mm, var 10mm]"),
12641            "Expected appended spline control point in source, got: {}",
12642            src_delta.text
12643        );
12644
12645        assert!(
12646            scene_delta.invalidates_ids,
12647            "Expected appending a spline control point to invalidate ids"
12648        );
12649        let updated_spline = scene_delta
12650            .new_graph
12651            .objects
12652            .iter()
12653            .find_map(|obj| match &obj.kind {
12654                ObjectKind::Segment {
12655                    segment: Segment::ControlPointSpline(updated_spline),
12656                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12657                _ => None,
12658            })
12659            .expect("Expected edited scene graph to contain a four-point control point spline");
12660        assert_eq!(
12661            updated_spline.controls.len(),
12662            4,
12663            "Expected edited spline to expose four control points"
12664        );
12665
12666        ctx.close().await;
12667        mock_ctx.close().await;
12668    }
12669
12670    #[tokio::test(flavor = "multi_thread")]
12671    async fn test_line_vertical() {
12672        let initial_source = "\
12673sketch(on = XY) {
12674  line(start = [var 1, var 2], end = [var 3, var 4])
12675}
12676";
12677
12678        let program = Program::parse(initial_source).unwrap().0.unwrap();
12679
12680        let mut frontend = FrontendState::new();
12681
12682        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12683        let mock_ctx = ExecutorContext::new_mock(None).await;
12684        let version = Version(0);
12685
12686        frontend.hack_set_program(&ctx, program).await.unwrap();
12687        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12688        let sketch_id = sketch_object.id;
12689        let sketch = expect_sketch(sketch_object);
12690        let line1_id = *sketch.segments.get(2).unwrap();
12691
12692        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12693        let (src_delta, scene_delta) = frontend
12694            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12695            .await
12696            .unwrap();
12697        assert_eq!(
12698            src_delta.text.as_str(),
12699            "\
12700sketch(on = XY) {
12701  line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12702  vertical(line1)
12703}
12704"
12705        );
12706        assert_eq!(
12707            scene_delta.new_graph.objects.len(),
12708            6,
12709            "{:#?}",
12710            scene_delta.new_graph.objects
12711        );
12712
12713        ctx.close().await;
12714        mock_ctx.close().await;
12715    }
12716
12717    #[tokio::test(flavor = "multi_thread")]
12718    async fn test_points_vertical() {
12719        let initial_source = "\
12720sketch001 = sketch(on = XY) {
12721  p0 = point(at = [var -2.23mm, var 3.1mm])
12722  pf = point(at = [4, 4])
12723}
12724";
12725
12726        let program = Program::parse(initial_source).unwrap().0.unwrap();
12727
12728        let mut frontend = FrontendState::new();
12729
12730        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12731        let mock_ctx = ExecutorContext::new_mock(None).await;
12732        let version = Version(0);
12733
12734        frontend.hack_set_program(&ctx, program).await.unwrap();
12735        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12736        let sketch_id = sketch_object.id;
12737        let sketch = expect_sketch(sketch_object);
12738        let point_ids = vec![
12739            sketch.segments.first().unwrap().to_owned(),
12740            sketch.segments.get(1).unwrap().to_owned(),
12741        ];
12742
12743        let constraint = Constraint::Vertical(Vertical::Points {
12744            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12745        });
12746        let (src_delta, scene_delta) = frontend
12747            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12748            .await
12749            .unwrap();
12750        assert_eq!(
12751            src_delta.text.as_str(),
12752            "\
12753sketch001 = sketch(on = XY) {
12754  p0 = point(at = [var 4mm, var 3.1mm])
12755  pf = point(at = [4, 4])
12756  vertical([p0, pf])
12757}
12758"
12759        );
12760        assert_eq!(
12761            scene_delta.new_graph.objects.len(),
12762            5,
12763            "{:#?}",
12764            scene_delta.new_graph.objects
12765        );
12766
12767        ctx.close().await;
12768        mock_ctx.close().await;
12769    }
12770
12771    #[tokio::test(flavor = "multi_thread")]
12772    async fn test_points_horizontal() {
12773        let initial_source = "\
12774sketch001 = sketch(on = XY) {
12775  p0 = point(at = [var -2.23mm, var 3.1mm])
12776  pf = point(at = [4, 4])
12777}
12778";
12779
12780        let program = Program::parse(initial_source).unwrap().0.unwrap();
12781
12782        let mut frontend = FrontendState::new();
12783
12784        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12785        let mock_ctx = ExecutorContext::new_mock(None).await;
12786        let version = Version(0);
12787
12788        frontend.hack_set_program(&ctx, program).await.unwrap();
12789        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12790        let sketch_id = sketch_object.id;
12791        let sketch = expect_sketch(sketch_object);
12792        let point_ids = vec![
12793            sketch.segments.first().unwrap().to_owned(),
12794            sketch.segments.get(1).unwrap().to_owned(),
12795        ];
12796
12797        let constraint = Constraint::Horizontal(Horizontal::Points {
12798            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12799        });
12800        let (src_delta, scene_delta) = frontend
12801            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12802            .await
12803            .unwrap();
12804        assert_eq!(
12805            src_delta.text.as_str(),
12806            "\
12807sketch001 = sketch(on = XY) {
12808  p0 = point(at = [var -2.23mm, var 4mm])
12809  pf = point(at = [4, 4])
12810  horizontal([p0, pf])
12811}
12812"
12813        );
12814        assert_eq!(
12815            scene_delta.new_graph.objects.len(),
12816            5,
12817            "{:#?}",
12818            scene_delta.new_graph.objects
12819        );
12820
12821        ctx.close().await;
12822        mock_ctx.close().await;
12823    }
12824
12825    #[tokio::test(flavor = "multi_thread")]
12826    async fn test_point_horizontal_with_origin() {
12827        let initial_source = "\
12828sketch001 = sketch(on = XY) {
12829  p0 = point(at = [var -2.23mm, var 3.1mm])
12830}
12831";
12832
12833        let program = Program::parse(initial_source).unwrap().0.unwrap();
12834
12835        let mut frontend = FrontendState::new();
12836
12837        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12838        let mock_ctx = ExecutorContext::new_mock(None).await;
12839        let version = Version(0);
12840
12841        frontend.hack_set_program(&ctx, program).await.unwrap();
12842        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12843        let sketch_id = sketch_object.id;
12844        let sketch = expect_sketch(sketch_object);
12845        let point_id = *sketch.segments.first().unwrap();
12846
12847        let constraint = Constraint::Horizontal(Horizontal::Points {
12848            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12849        });
12850        let (src_delta, scene_delta) = frontend
12851            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12852            .await
12853            .unwrap();
12854        assert_eq!(
12855            src_delta.text.as_str(),
12856            "\
12857sketch001 = sketch(on = XY) {
12858  p0 = point(at = [var -2.23mm, var 0mm])
12859  horizontal([p0, ORIGIN])
12860}
12861"
12862        );
12863        assert_eq!(
12864            scene_delta.new_graph.objects.len(),
12865            4,
12866            "{:#?}",
12867            scene_delta.new_graph.objects
12868        );
12869
12870        ctx.close().await;
12871        mock_ctx.close().await;
12872    }
12873
12874    #[tokio::test(flavor = "multi_thread")]
12875    async fn test_lines_equal_length() {
12876        let initial_source = "\
12877sketch(on = XY) {
12878  line(start = [var 1, var 2], end = [var 3, var 4])
12879  line(start = [var 5, var 6], end = [var 7, var 8])
12880}
12881";
12882
12883        let program = Program::parse(initial_source).unwrap().0.unwrap();
12884
12885        let mut frontend = FrontendState::new();
12886
12887        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12888        let mock_ctx = ExecutorContext::new_mock(None).await;
12889        let version = Version(0);
12890
12891        frontend.hack_set_program(&ctx, program).await.unwrap();
12892        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12893        let sketch_id = sketch_object.id;
12894        let sketch = expect_sketch(sketch_object);
12895        let line1_id = *sketch.segments.get(2).unwrap();
12896        let line2_id = *sketch.segments.get(5).unwrap();
12897
12898        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12899            lines: vec![line1_id, line2_id],
12900        });
12901        let (src_delta, scene_delta) = frontend
12902            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12903            .await
12904            .unwrap();
12905        assert_eq!(
12906            src_delta.text.as_str(),
12907            "\
12908sketch(on = XY) {
12909  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12910  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12911  equalLength([line1, line2])
12912}
12913"
12914        );
12915        assert_eq!(
12916            scene_delta.new_graph.objects.len(),
12917            9,
12918            "{:#?}",
12919            scene_delta.new_graph.objects
12920        );
12921
12922        ctx.close().await;
12923        mock_ctx.close().await;
12924    }
12925
12926    #[tokio::test(flavor = "multi_thread")]
12927    async fn test_add_constraint_multi_line_equal_length() {
12928        let initial_source = "\
12929sketch(on = XY) {
12930  line(start = [var 1, var 2], end = [var 3, var 4])
12931  line(start = [var 5, var 6], end = [var 7, var 8])
12932  line(start = [var 9, var 10], end = [var 11, var 12])
12933}
12934";
12935
12936        let program = Program::parse(initial_source).unwrap().0.unwrap();
12937
12938        let mut frontend = FrontendState::new();
12939        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12940        let mock_ctx = ExecutorContext::new_mock(None).await;
12941        let version = Version(0);
12942
12943        frontend.hack_set_program(&ctx, program).await.unwrap();
12944        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12945        let sketch_id = sketch_object.id;
12946        let sketch = expect_sketch(sketch_object);
12947        let line1_id = *sketch.segments.get(2).unwrap();
12948        let line2_id = *sketch.segments.get(5).unwrap();
12949        let line3_id = *sketch.segments.get(8).unwrap();
12950
12951        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12952            lines: vec![line1_id, line2_id, line3_id],
12953        });
12954        let (src_delta, scene_delta) = frontend
12955            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12956            .await
12957            .unwrap();
12958        assert_eq!(
12959            src_delta.text.as_str(),
12960            "\
12961sketch(on = XY) {
12962  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12963  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12964  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12965  equalLength([line1, line2, line3])
12966}
12967"
12968        );
12969        let constraints = scene_delta
12970            .new_graph
12971            .objects
12972            .iter()
12973            .filter_map(|obj| {
12974                let ObjectKind::Constraint { constraint } = &obj.kind else {
12975                    return None;
12976                };
12977                Some(constraint)
12978            })
12979            .collect::<Vec<_>>();
12980
12981        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12982        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12983            panic!("expected equal length constraint, got {:?}", constraints[0]);
12984        };
12985        assert_eq!(lines_equal_length.lines.len(), 3);
12986
12987        ctx.close().await;
12988        mock_ctx.close().await;
12989    }
12990
12991    #[tokio::test(flavor = "multi_thread")]
12992    async fn test_lines_parallel() {
12993        let initial_source = "\
12994sketch(on = XY) {
12995  line(start = [var 1, var 2], end = [var 3, var 4])
12996  line(start = [var 5, var 6], end = [var 7, var 8])
12997}
12998";
12999
13000        let program = Program::parse(initial_source).unwrap().0.unwrap();
13001
13002        let mut frontend = FrontendState::new();
13003
13004        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13005        let mock_ctx = ExecutorContext::new_mock(None).await;
13006        let version = Version(0);
13007
13008        frontend.hack_set_program(&ctx, program).await.unwrap();
13009        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13010        let sketch_id = sketch_object.id;
13011        let sketch = expect_sketch(sketch_object);
13012        let line1_id = *sketch.segments.get(2).unwrap();
13013        let line2_id = *sketch.segments.get(5).unwrap();
13014
13015        let constraint = Constraint::Parallel(Parallel {
13016            lines: vec![line1_id, line2_id],
13017        });
13018        let (src_delta, scene_delta) = frontend
13019            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13020            .await
13021            .unwrap();
13022        assert_eq!(
13023            src_delta.text.as_str(),
13024            "\
13025sketch(on = XY) {
13026  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13027  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13028  parallel([line1, line2])
13029}
13030"
13031        );
13032        assert_eq!(
13033            scene_delta.new_graph.objects.len(),
13034            9,
13035            "{:#?}",
13036            scene_delta.new_graph.objects
13037        );
13038
13039        ctx.close().await;
13040        mock_ctx.close().await;
13041    }
13042
13043    #[tokio::test(flavor = "multi_thread")]
13044    async fn test_lines_parallel_multiline() {
13045        let initial_source = "\
13046sketch(on = XY) {
13047  line(start = [var 1, var 2], end = [var 3, var 4])
13048  line(start = [var 5, var 6], end = [var 7, var 8])
13049  line(start = [var 9, var 10], end = [var 11, var 12])
13050}
13051";
13052
13053        let program = Program::parse(initial_source).unwrap().0.unwrap();
13054
13055        let mut frontend = FrontendState::new();
13056
13057        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13058        let mock_ctx = ExecutorContext::new_mock(None).await;
13059        let version = Version(0);
13060
13061        frontend.hack_set_program(&ctx, program).await.unwrap();
13062        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13063        let sketch_id = sketch_object.id;
13064        let sketch = expect_sketch(sketch_object);
13065        let line1_id = *sketch.segments.get(2).unwrap();
13066        let line2_id = *sketch.segments.get(5).unwrap();
13067        let line3_id = *sketch.segments.get(8).unwrap();
13068
13069        let constraint = Constraint::Parallel(Parallel {
13070            lines: vec![line1_id, line2_id, line3_id],
13071        });
13072        let (src_delta, scene_delta) = frontend
13073            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13074            .await
13075            .unwrap();
13076        assert_eq!(
13077            src_delta.text.as_str(),
13078            "\
13079sketch(on = XY) {
13080  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13081  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13082  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13083  parallel([line1, line2, line3])
13084}
13085"
13086        );
13087
13088        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13089        let sketch = expect_sketch(sketch_object);
13090        assert_eq!(sketch.constraints.len(), 1);
13091
13092        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13093        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13094            panic!("Expected constraint object");
13095        };
13096        let Constraint::Parallel(parallel) = constraint else {
13097            panic!("Expected parallel constraint");
13098        };
13099        assert_eq!(parallel.lines.len(), 3);
13100
13101        ctx.close().await;
13102        mock_ctx.close().await;
13103    }
13104
13105    #[tokio::test(flavor = "multi_thread")]
13106    async fn test_lines_perpendicular() {
13107        let initial_source = "\
13108sketch(on = XY) {
13109  line(start = [var 1, var 2], end = [var 3, var 4])
13110  line(start = [var 5, var 6], end = [var 7, var 8])
13111}
13112";
13113
13114        let program = Program::parse(initial_source).unwrap().0.unwrap();
13115
13116        let mut frontend = FrontendState::new();
13117
13118        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13119        let mock_ctx = ExecutorContext::new_mock(None).await;
13120        let version = Version(0);
13121
13122        frontend.hack_set_program(&ctx, program).await.unwrap();
13123        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13124        let sketch_id = sketch_object.id;
13125        let sketch = expect_sketch(sketch_object);
13126        let line1_id = *sketch.segments.get(2).unwrap();
13127        let line2_id = *sketch.segments.get(5).unwrap();
13128
13129        let constraint = Constraint::Perpendicular(Perpendicular {
13130            lines: vec![line1_id, line2_id],
13131        });
13132        let (src_delta, scene_delta) = frontend
13133            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13134            .await
13135            .unwrap();
13136        assert_eq!(
13137            src_delta.text.as_str(),
13138            "\
13139sketch(on = XY) {
13140  line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13141  line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13142  perpendicular([line1, line2])
13143}
13144"
13145        );
13146        assert_eq!(
13147            scene_delta.new_graph.objects.len(),
13148            9,
13149            "{:#?}",
13150            scene_delta.new_graph.objects
13151        );
13152
13153        ctx.close().await;
13154        mock_ctx.close().await;
13155    }
13156
13157    #[tokio::test(flavor = "multi_thread")]
13158    async fn test_lines_angle() {
13159        let initial_source = "\
13160sketch(on = XY) {
13161  line(start = [var 1, var 2], end = [var 3, var 4])
13162  line(start = [var 5, var 6], end = [var 7, var 8])
13163}
13164";
13165
13166        let program = Program::parse(initial_source).unwrap().0.unwrap();
13167
13168        let mut frontend = FrontendState::new();
13169
13170        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13171        let mock_ctx = ExecutorContext::new_mock(None).await;
13172        let version = Version(0);
13173
13174        frontend.hack_set_program(&ctx, program).await.unwrap();
13175        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13176        let sketch_id = sketch_object.id;
13177        let sketch = expect_sketch(sketch_object);
13178        let line1_id = *sketch.segments.get(2).unwrap();
13179        let line2_id = *sketch.segments.get(5).unwrap();
13180
13181        let constraint = Constraint::Angle(Angle {
13182            lines: vec![line1_id, line2_id],
13183            angle: Number {
13184                value: 30.0,
13185                units: NumericSuffix::Deg,
13186            },
13187            source: Default::default(),
13188        });
13189        let (src_delta, scene_delta) = frontend
13190            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13191            .await
13192            .unwrap();
13193        assert_eq!(
13194            src_delta.text.as_str(),
13195            // The lack indentation is a formatter bug.
13196            "\
13197sketch(on = XY) {
13198  line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13199  line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13200  angle([line1, line2]) == 30deg
13201}
13202"
13203        );
13204        assert_eq!(
13205            scene_delta.new_graph.objects.len(),
13206            9,
13207            "{:#?}",
13208            scene_delta.new_graph.objects
13209        );
13210
13211        ctx.close().await;
13212        mock_ctx.close().await;
13213    }
13214
13215    #[tokio::test(flavor = "multi_thread")]
13216    async fn test_segments_tangent() {
13217        let initial_source = "\
13218sketch(on = XY) {
13219  line(start = [var 1, var 2], end = [var 3, var 4])
13220  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13221}
13222";
13223
13224        let program = Program::parse(initial_source).unwrap().0.unwrap();
13225
13226        let mut frontend = FrontendState::new();
13227
13228        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13229        let mock_ctx = ExecutorContext::new_mock(None).await;
13230        let version = Version(0);
13231
13232        frontend.hack_set_program(&ctx, program).await.unwrap();
13233        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13234        let sketch_id = sketch_object.id;
13235        let sketch = expect_sketch(sketch_object);
13236        let line1_id = *sketch.segments.get(2).unwrap();
13237        let arc1_id = *sketch.segments.get(6).unwrap();
13238
13239        let constraint = Constraint::Tangent(Tangent {
13240            input: vec![line1_id, arc1_id],
13241        });
13242        let (src_delta, scene_delta) = frontend
13243            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13244            .await
13245            .unwrap();
13246        assert_eq!(
13247            src_delta.text.as_str(),
13248            "\
13249sketch(on = XY) {
13250  line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13251  arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13252  tangent([line1, arc1])
13253}
13254"
13255        );
13256        assert_eq!(
13257            scene_delta.new_graph.objects.len(),
13258            10,
13259            "{:#?}",
13260            scene_delta.new_graph.objects
13261        );
13262
13263        ctx.close().await;
13264        mock_ctx.close().await;
13265    }
13266
13267    #[tokio::test(flavor = "multi_thread")]
13268    async fn test_point_midpoint() {
13269        let initial_source = "\
13270sketch(on = XY) {
13271  point(at = [var 1, var 1])
13272  line(start = [var 0, var 0], end = [var 6, var 4])
13273}
13274";
13275
13276        let program = Program::parse(initial_source).unwrap().0.unwrap();
13277
13278        let mut frontend = FrontendState::new();
13279
13280        let ctx = ExecutorContext::new_mock(None).await;
13281        let version = Version(0);
13282
13283        frontend.program = program.clone();
13284        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13285        frontend.update_state_after_exec(outcome, true);
13286        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13287        let sketch_id = sketch_object.id;
13288        let sketch = expect_sketch(sketch_object);
13289        let point_id = *sketch.segments.first().unwrap();
13290        let line_id = *sketch.segments.get(3).unwrap();
13291
13292        let constraint = Constraint::Midpoint(Midpoint {
13293            point: ConstraintSegment::from(point_id),
13294            segment: line_id,
13295        });
13296        let (src_delta, scene_delta) = frontend
13297            .add_constraint(&ctx, version, sketch_id, constraint)
13298            .await
13299            .unwrap();
13300        assert_eq!(
13301            src_delta.text.as_str(),
13302            "\
13303sketch(on = XY) {
13304  point1 = point(at = [var 2.33, var 1.67])
13305  line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13306  midpoint(line1, point = point1)
13307}
13308"
13309        );
13310        assert_eq!(
13311            scene_delta.new_graph.objects.len(),
13312            7,
13313            "{:#?}",
13314            scene_delta.new_graph.objects
13315        );
13316
13317        ctx.close().await;
13318    }
13319
13320    #[tokio::test(flavor = "multi_thread")]
13321    async fn test_segments_symmetric() {
13322        let initial_source = "\
13323sketch(on = XY) {
13324  line(start = [var 0, var 0], end = [var 0, var 4])
13325  line(start = [var 4, var 0], end = [var 4, var 4])
13326  line(start = [var 2, var -1], end = [var 2, var 5])
13327}
13328";
13329
13330        let program = Program::parse(initial_source).unwrap().0.unwrap();
13331
13332        let mut frontend = FrontendState::new();
13333
13334        let ctx = ExecutorContext::new_mock(None).await;
13335        let version = Version(0);
13336
13337        frontend.program = program.clone();
13338        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13339        frontend.update_state_after_exec(outcome, true);
13340        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13341        let sketch_id = sketch_object.id;
13342        let sketch = expect_sketch(sketch_object);
13343        let line1_id = *sketch.segments.get(2).unwrap();
13344        let line2_id = *sketch.segments.get(5).unwrap();
13345        let axis_id = *sketch.segments.get(8).unwrap();
13346
13347        let constraint = Constraint::Symmetric(Symmetric {
13348            input: vec![line1_id, line2_id],
13349            axis: axis_id,
13350        });
13351        let (src_delta, scene_delta) = frontend
13352            .add_constraint(&ctx, version, sketch_id, constraint)
13353            .await
13354            .unwrap();
13355        assert_eq!(
13356            src_delta.text.as_str(),
13357            "\
13358sketch(on = XY) {
13359  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13360  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13361  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13362  symmetric([line1, line2], axis = line3)
13363}
13364"
13365        );
13366        assert_eq!(
13367            scene_delta.new_graph.objects.len(),
13368            12,
13369            "{:#?}",
13370            scene_delta.new_graph.objects
13371        );
13372
13373        ctx.close().await;
13374    }
13375
13376    #[tokio::test(flavor = "multi_thread")]
13377    async fn test_point_arc_midpoint() {
13378        let initial_source = "\
13379sketch(on = XY) {
13380  point(at = [var 6, var 3])
13381  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13382}
13383";
13384
13385        let program = Program::parse(initial_source).unwrap().0.unwrap();
13386
13387        let mut frontend = FrontendState::new();
13388
13389        let ctx = ExecutorContext::new_mock(None).await;
13390        let version = Version(0);
13391
13392        frontend.program = program.clone();
13393        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13394        frontend.update_state_after_exec(outcome, true);
13395        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13396        let sketch_id = sketch_object.id;
13397        let sketch = expect_sketch(sketch_object);
13398        let point_id = *sketch.segments.first().unwrap();
13399        let arc_id = *sketch.segments.get(4).unwrap();
13400
13401        let constraint = Constraint::Midpoint(Midpoint {
13402            point: ConstraintSegment::from(point_id),
13403            segment: arc_id,
13404        });
13405        let (src_delta, scene_delta) = frontend
13406            .add_constraint(&ctx, version, sketch_id, constraint)
13407            .await
13408            .unwrap();
13409        assert_eq!(
13410            src_delta.text.as_str(),
13411            "\
13412sketch(on = XY) {
13413  point1 = point(at = [var 6, var 2.35])
13414  arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13415  midpoint(arc1, point = point1)
13416}
13417"
13418        );
13419        assert_eq!(
13420            scene_delta.new_graph.objects.len(),
13421            8,
13422            "{:#?}",
13423            scene_delta.new_graph.objects
13424        );
13425
13426        ctx.close().await;
13427    }
13428
13429    #[tokio::test(flavor = "multi_thread")]
13430    async fn test_origin_line_midpoint() {
13431        let initial_source = "\
13432sketch(on = XY) {
13433  line(start = [var 0, var 0], end = [var 6, var 4])
13434}
13435";
13436
13437        let program = Program::parse(initial_source).unwrap().0.unwrap();
13438
13439        let mut frontend = FrontendState::new();
13440
13441        let ctx = ExecutorContext::new_mock(None).await;
13442        let version = Version(0);
13443
13444        frontend.program = program.clone();
13445        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13446        frontend.update_state_after_exec(outcome, true);
13447        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13448        let sketch_id = sketch_object.id;
13449        let sketch = expect_sketch(sketch_object);
13450        let line_id = *sketch.segments.get(2).unwrap();
13451
13452        let constraint = Constraint::Midpoint(Midpoint {
13453            point: ConstraintSegment::ORIGIN,
13454            segment: line_id,
13455        });
13456        let (src_delta, scene_delta) = frontend
13457            .add_constraint(&ctx, version, sketch_id, constraint)
13458            .await
13459            .unwrap();
13460        assert_eq!(
13461            src_delta.text.as_str(),
13462            "\
13463sketch(on = XY) {
13464  line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13465  midpoint(line1, point = ORIGIN)
13466}
13467"
13468        );
13469        assert_eq!(
13470            scene_delta.new_graph.objects.len(),
13471            6,
13472            "{:#?}",
13473            scene_delta.new_graph.objects
13474        );
13475
13476        ctx.close().await;
13477    }
13478
13479    #[tokio::test(flavor = "multi_thread")]
13480    async fn test_origin_arc_midpoint() {
13481        let initial_source = "\
13482sketch(on = XY) {
13483  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13484}
13485";
13486
13487        let program = Program::parse(initial_source).unwrap().0.unwrap();
13488
13489        let mut frontend = FrontendState::new();
13490
13491        let ctx = ExecutorContext::new_mock(None).await;
13492        let version = Version(0);
13493
13494        frontend.program = program.clone();
13495        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13496        frontend.update_state_after_exec(outcome, true);
13497        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13498        let sketch_id = sketch_object.id;
13499        let sketch = expect_sketch(sketch_object);
13500        let arc_id = *sketch.segments.get(3).unwrap();
13501
13502        let constraint = Constraint::Midpoint(Midpoint {
13503            point: ConstraintSegment::ORIGIN,
13504            segment: arc_id,
13505        });
13506        let (src_delta, scene_delta) = frontend
13507            .add_constraint(&ctx, version, sketch_id, constraint)
13508            .await
13509            .unwrap();
13510        assert_eq!(
13511            src_delta.text.as_str(),
13512            "\
13513sketch(on = XY) {
13514  arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13515  midpoint(arc1, point = ORIGIN)
13516}
13517"
13518        );
13519        assert_eq!(
13520            scene_delta.new_graph.objects.len(),
13521            7,
13522            "{:#?}",
13523            scene_delta.new_graph.objects
13524        );
13525
13526        ctx.close().await;
13527    }
13528
13529    #[tokio::test(flavor = "multi_thread")]
13530    async fn test_segments_symmetric_arcs() {
13531        let initial_source = "\
13532sketch(on = XY) {
13533  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13534  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13535  line(start = [var 0, var -10], end = [var 0, var 10])
13536}
13537";
13538
13539        let program = Program::parse(initial_source).unwrap().0.unwrap();
13540
13541        let mut frontend = FrontendState::new();
13542
13543        let ctx = ExecutorContext::new_mock(None).await;
13544        let version = Version(0);
13545
13546        frontend.program = program.clone();
13547        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13548        frontend.update_state_after_exec(outcome, true);
13549        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13550        let sketch_id = sketch_object.id;
13551        let sketch = expect_sketch(sketch_object);
13552        let arc1_id = *sketch.segments.get(3).unwrap();
13553        let arc2_id = *sketch.segments.get(7).unwrap();
13554        let axis_id = *sketch.segments.get(10).unwrap();
13555
13556        let constraint = Constraint::Symmetric(Symmetric {
13557            input: vec![arc1_id, arc2_id],
13558            axis: axis_id,
13559        });
13560        let (src_delta, scene_delta) = frontend
13561            .add_constraint(&ctx, version, sketch_id, constraint)
13562            .await
13563            .unwrap();
13564        assert_eq!(
13565            src_delta.text.as_str(),
13566            "\
13567sketch(on = XY) {
13568  arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13569  arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13570  line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13571  symmetric([arc1, arc2], axis = line1)
13572}
13573"
13574        );
13575        assert_eq!(
13576            scene_delta.new_graph.objects.len(),
13577            14,
13578            "{:#?}",
13579            scene_delta.new_graph.objects
13580        );
13581
13582        ctx.close().await;
13583    }
13584
13585    #[tokio::test(flavor = "multi_thread")]
13586    async fn test_sketch_on_face_simple() {
13587        let initial_source = "\
13588len = 2mm
13589cube = startSketchOn(XY)
13590  |> startProfile(at = [0, 0])
13591  |> line(end = [len, 0], tag = $side)
13592  |> line(end = [0, len])
13593  |> line(end = [-len, 0])
13594  |> line(end = [0, -len])
13595  |> close()
13596  |> extrude(length = len)
13597
13598face = faceOf(cube, face = side)
13599";
13600
13601        let program = Program::parse(initial_source).unwrap().0.unwrap();
13602
13603        let mut frontend = FrontendState::new();
13604
13605        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13606        let mock_ctx = ExecutorContext::new_mock(None).await;
13607        let version = Version(0);
13608
13609        frontend.hack_set_program(&ctx, program).await.unwrap();
13610        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13611        let face_id = face_object.id;
13612
13613        let sketch_args = SketchCtor {
13614            on: Plane::Object(face_id),
13615        };
13616        let (_src_delta, scene_delta, sketch_id) = frontend
13617            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13618            .await
13619            .unwrap();
13620        assert_eq!(sketch_id, ObjectId(2));
13621        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13622        let sketch_object = &scene_delta.new_graph.objects[2];
13623        assert_eq!(sketch_object.id, ObjectId(2));
13624        assert_eq!(
13625            sketch_object.kind,
13626            ObjectKind::Sketch(Sketch {
13627                args: SketchCtor {
13628                    on: Plane::Object(face_id),
13629                },
13630                plane: face_id,
13631                segments: vec![],
13632                constraints: vec![],
13633            })
13634        );
13635        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13636
13637        ctx.close().await;
13638        mock_ctx.close().await;
13639    }
13640
13641    #[tokio::test(flavor = "multi_thread")]
13642    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13643        let initial_source = "\
13644s = sketch(on = YZ) {
13645  line1 = line(start = [0, 0], end = [0, 1])
13646  line2 = line(start = [0, 1], end = [1, 1])
13647  line3 = line(start = [1, 1], end = [0, 0])
13648}
13649region001 = region(point = [0.1, 0.1], sketch = s)
13650extrude001 = extrude(region001, length = 5)
13651";
13652
13653        let program = Program::parse(initial_source).unwrap().0.unwrap();
13654
13655        let mut frontend = FrontendState::new();
13656        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13657        let version = Version(0);
13658
13659        frontend.hack_set_program(&ctx, program).await.unwrap();
13660        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13661
13662        let sketch_args = SketchCtor {
13663            on: Plane::Object(wall_object_id),
13664        };
13665        let (src_delta, _scene_delta, _sketch_id) = frontend
13666            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13667            .await
13668            .unwrap();
13669        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13670
13671        ctx.close().await;
13672    }
13673
13674    #[tokio::test(flavor = "multi_thread")]
13675    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13676        let initial_source = "\
13677sketch001 = sketch(on = YZ) {
13678  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13679  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13680  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13681  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13682  coincident([line1.end, line2.start])
13683  coincident([line2.end, line3.start])
13684  coincident([line3.end, line4.start])
13685  coincident([line4.end, line1.start])
13686  parallel([line2, line4])
13687  parallel([line3, line1])
13688  perpendicular([line1, line2])
13689  horizontal(line3)
13690  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13691}
13692region001 = region(point = [3.1, 3.74], sketch = sketch001)
13693extrude001 = extrude(region001, length = 5)
13694";
13695
13696        let program = Program::parse(initial_source).unwrap().0.unwrap();
13697
13698        let mut frontend = FrontendState::new();
13699        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13700        let version = Version(0);
13701
13702        frontend.hack_set_program(&ctx, program).await.unwrap();
13703        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13704
13705        let sketch_args = SketchCtor {
13706            on: Plane::Object(wall_object_id),
13707        };
13708        let (src_delta, _scene_delta, _sketch_id) = frontend
13709            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13710            .await
13711            .unwrap();
13712        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13713
13714        ctx.close().await;
13715    }
13716
13717    #[tokio::test(flavor = "multi_thread")]
13718    async fn test_sketch_on_plane_incremental() {
13719        let initial_source = "\
13720len = 2mm
13721cube = startSketchOn(XY)
13722  |> startProfile(at = [0, 0])
13723  |> line(end = [len, 0], tag = $side)
13724  |> line(end = [0, len])
13725  |> line(end = [-len, 0])
13726  |> line(end = [0, -len])
13727  |> close()
13728  |> extrude(length = len)
13729
13730plane = planeOf(cube, face = side)
13731";
13732
13733        let program = Program::parse(initial_source).unwrap().0.unwrap();
13734
13735        let mut frontend = FrontendState::new();
13736
13737        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13738        let mock_ctx = ExecutorContext::new_mock(None).await;
13739        let version = Version(0);
13740
13741        frontend.hack_set_program(&ctx, program).await.unwrap();
13742        // Find the last plane since the first plane is the XY plane.
13743        let plane_object = frontend
13744            .scene_graph
13745            .objects
13746            .iter()
13747            .rev()
13748            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13749            .unwrap();
13750        let plane_id = plane_object.id;
13751
13752        let sketch_args = SketchCtor {
13753            on: Plane::Object(plane_id),
13754        };
13755        let (src_delta, scene_delta, sketch_id) = frontend
13756            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13757            .await
13758            .unwrap();
13759        assert_eq!(
13760            src_delta.text.as_str(),
13761            "\
13762len = 2mm
13763cube = startSketchOn(XY)
13764  |> startProfile(at = [0, 0])
13765  |> line(end = [len, 0], tag = $side)
13766  |> line(end = [0, len])
13767  |> line(end = [-len, 0])
13768  |> line(end = [0, -len])
13769  |> close()
13770  |> extrude(length = len)
13771
13772plane = planeOf(cube, face = side)
13773sketch001 = sketch(on = plane) {
13774}
13775"
13776        );
13777        assert_eq!(sketch_id, ObjectId(2));
13778        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13779        let sketch_object = &scene_delta.new_graph.objects[2];
13780        assert_eq!(sketch_object.id, ObjectId(2));
13781        assert_eq!(
13782            sketch_object.kind,
13783            ObjectKind::Sketch(Sketch {
13784                args: SketchCtor {
13785                    on: Plane::Object(plane_id),
13786                },
13787                plane: plane_id,
13788                segments: vec![],
13789                constraints: vec![],
13790            })
13791        );
13792        assert_eq!(scene_delta.new_graph.objects.len(), 9);
13793
13794        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13795        assert_eq!(plane_object.id, plane_id);
13796        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13797
13798        ctx.close().await;
13799        mock_ctx.close().await;
13800    }
13801
13802    #[tokio::test(flavor = "multi_thread")]
13803    async fn test_new_sketch_uses_unique_variable_name() {
13804        let initial_source = "\
13805sketch1 = sketch(on = XY) {
13806}
13807";
13808
13809        let program = Program::parse(initial_source).unwrap().0.unwrap();
13810
13811        let mut frontend = FrontendState::new();
13812        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13813        let version = Version(0);
13814
13815        frontend.hack_set_program(&ctx, program).await.unwrap();
13816
13817        let sketch_args = SketchCtor {
13818            on: Plane::Default(PlaneName::Yz),
13819        };
13820        let (src_delta, _, _) = frontend
13821            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13822            .await
13823            .unwrap();
13824
13825        assert_eq!(
13826            src_delta.text.as_str(),
13827            "\
13828sketch1 = sketch(on = XY) {
13829}
13830sketch001 = sketch(on = YZ) {
13831}
13832"
13833        );
13834
13835        ctx.close().await;
13836    }
13837
13838    #[tokio::test(flavor = "multi_thread")]
13839    async fn test_new_sketch_twice_using_same_plane() {
13840        let initial_source = "\
13841sketch1 = sketch(on = XY) {
13842}
13843";
13844
13845        let program = Program::parse(initial_source).unwrap().0.unwrap();
13846
13847        let mut frontend = FrontendState::new();
13848        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13849        let version = Version(0);
13850
13851        frontend.hack_set_program(&ctx, program).await.unwrap();
13852
13853        let sketch_args = SketchCtor {
13854            on: Plane::Default(PlaneName::Xy),
13855        };
13856        let (src_delta, _, _) = frontend
13857            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13858            .await
13859            .unwrap();
13860
13861        assert_eq!(
13862            src_delta.text.as_str(),
13863            "\
13864sketch1 = sketch(on = XY) {
13865}
13866sketch001 = sketch(on = XY) {
13867}
13868"
13869        );
13870
13871        ctx.close().await;
13872    }
13873
13874    #[tokio::test(flavor = "multi_thread")]
13875    async fn test_sketch_mode_reuses_cached_on_expression() {
13876        let initial_source = "\
13877width = 2mm
13878sketch(on = offsetPlane(XY, offset = width)) {
13879  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13880  distance([line1.start, line1.end]) == width
13881}
13882";
13883        let program = Program::parse(initial_source).unwrap().0.unwrap();
13884
13885        let mut frontend = FrontendState::new();
13886        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13887        let mock_ctx = ExecutorContext::new_mock(None).await;
13888        let version = Version(0);
13889        let project_id = ProjectId(0);
13890        let file_id = FileId(0);
13891
13892        frontend.hack_set_program(&ctx, program).await.unwrap();
13893        let initial_object_count = frontend.scene_graph.objects.len();
13894        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13895            .expect("Expected sketch object to exist")
13896            .id;
13897
13898        // Entering sketch mode should reuse cached `on` expression state
13899        // (offsetPlane result), not fail or create extra on-surface objects.
13900        let scene_delta = frontend
13901            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13902            .await
13903            .unwrap();
13904        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13905
13906        // A follow-up sketch-mode execution should keep the same stable object
13907        // graph shape as well.
13908        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13909        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13910
13911        ctx.close().await;
13912        mock_ctx.close().await;
13913    }
13914
13915    #[tokio::test(flavor = "multi_thread")]
13916    async fn test_multiple_sketch_blocks() {
13917        let initial_source = "\
13918// Cube that requires the engine.
13919width = 2
13920sketch001 = startSketchOn(XY)
13921profile001 = startProfile(sketch001, at = [0, 0])
13922  |> yLine(length = width, tag = $seg1)
13923  |> xLine(length = width)
13924  |> yLine(length = -width)
13925  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13926  |> close()
13927extrude001 = extrude(profile001, length = width)
13928
13929// Get a value that requires the engine.
13930x = segLen(seg1)
13931
13932// Triangle with side length 2*x.
13933sketch(on = XY) {
13934  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13935  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13936  coincident([line1.end, line2.start])
13937  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13938  coincident([line2.end, line3.start])
13939  coincident([line3.end, line1.start])
13940  equalLength([line3, line1])
13941  equalLength([line1, line2])
13942  distance([line1.start, line1.end]) == 2*x
13943}
13944
13945// Line segment with length x.
13946sketch2 = sketch(on = XY) {
13947  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13948  distance([line1.start, line1.end]) == x
13949}
13950";
13951
13952        let program = Program::parse(initial_source).unwrap().0.unwrap();
13953
13954        let mut frontend = FrontendState::new();
13955
13956        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13957        let mock_ctx = ExecutorContext::new_mock(None).await;
13958        let version = Version(0);
13959        let project_id = ProjectId(0);
13960        let file_id = FileId(0);
13961
13962        frontend.hack_set_program(&ctx, program).await.unwrap();
13963        let sketch_objects = frontend
13964            .scene_graph
13965            .objects
13966            .iter()
13967            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13968            .collect::<Vec<_>>();
13969        let sketch1_id = sketch_objects.first().unwrap().id;
13970        let sketch2_id = sketch_objects.get(1).unwrap().id;
13971        // First point in sketch1.
13972        let point1_id = ObjectId(sketch1_id.0 + 1);
13973        // First point in sketch2.
13974        let point2_id = ObjectId(sketch2_id.0 + 1);
13975
13976        // Edit the first sketch. Objects before the sketch block should be
13977        // present from execution cache so that we can sketch on prior planes,
13978        // for example. Objects after the first sketch block should not be
13979        // present since those statements are skipped in sketch mode.
13980        //
13981        // - startSketchOn(XY) Plane 1
13982        // - sketch on=XY Plane 1
13983        // - Sketch block 16
13984        let scene_delta = frontend
13985            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13986            .await
13987            .unwrap();
13988        assert_eq!(
13989            scene_delta.new_graph.objects.len(),
13990            18,
13991            "{:#?}",
13992            scene_delta.new_graph.objects
13993        );
13994
13995        // Edit a point in the first sketch.
13996        let point_ctor = PointCtor {
13997            position: Point2d {
13998                x: Expr::Var(Number {
13999                    value: 1.0,
14000                    units: NumericSuffix::Mm,
14001                }),
14002                y: Expr::Var(Number {
14003                    value: 2.0,
14004                    units: NumericSuffix::Mm,
14005                }),
14006            },
14007        };
14008        let segments = vec![ExistingSegmentCtor {
14009            id: point1_id,
14010            ctor: SegmentCtor::Point(point_ctor),
14011        }];
14012        let (src_delta, _) = frontend
14013            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14014            .await
14015            .unwrap();
14016        // Only the first sketch block changes.
14017        assert_eq!(
14018            src_delta.text.as_str(),
14019            "\
14020// Cube that requires the engine.
14021width = 2
14022sketch001 = startSketchOn(XY)
14023profile001 = startProfile(sketch001, at = [0, 0])
14024  |> yLine(length = width, tag = $seg1)
14025  |> xLine(length = width)
14026  |> yLine(length = -width)
14027  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14028  |> close()
14029extrude001 = extrude(profile001, length = width)
14030
14031// Get a value that requires the engine.
14032x = segLen(seg1)
14033
14034// Triangle with side length 2*x.
14035sketch(on = XY) {
14036  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14037  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14038  coincident([line1.end, line2.start])
14039  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14040  coincident([line2.end, line3.start])
14041  coincident([line3.end, line1.start])
14042  equalLength([line3, line1])
14043  equalLength([line1, line2])
14044  distance([line1.start, line1.end]) == 2 * x
14045}
14046
14047// Line segment with length x.
14048sketch2 = sketch(on = XY) {
14049  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14050  distance([line1.start, line1.end]) == x
14051}
14052"
14053        );
14054        let edited_sketch1_source = src_delta.text.clone();
14055
14056        // Execute mock to simulate drag end.
14057        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14058        assert_eq!(src_delta.text, edited_sketch1_source);
14059        // Exit sketch. Objects from the entire program should be present.
14060        //
14061        // - startSketchOn(XY) Plane 1
14062        // - sketch on=XY Plane 1
14063        // - Sketch block 16
14064        // - sketch on=XY cached
14065        // - Sketch block 5
14066        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14067        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14068
14069        // Edit the second sketch.
14070        //
14071        // - startSketchOn(XY) Plane 1
14072        // - sketch on=XY Plane 1
14073        // - Sketch block 16
14074        // - sketch on=XY cached
14075        // - Sketch block 5
14076        let scene_delta = frontend
14077            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14078            .await
14079            .unwrap();
14080        assert_eq!(
14081            scene_delta.new_graph.objects.len(),
14082            24,
14083            "{:#?}",
14084            scene_delta.new_graph.objects
14085        );
14086
14087        // Edit a point in the second sketch.
14088        let point_ctor = PointCtor {
14089            position: Point2d {
14090                x: Expr::Var(Number {
14091                    value: 3.0,
14092                    units: NumericSuffix::Mm,
14093                }),
14094                y: Expr::Var(Number {
14095                    value: 4.0,
14096                    units: NumericSuffix::Mm,
14097                }),
14098            },
14099        };
14100        let segments = vec![ExistingSegmentCtor {
14101            id: point2_id,
14102            ctor: SegmentCtor::Point(point_ctor),
14103        }];
14104        let (src_delta, _) = frontend
14105            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14106            .await
14107            .unwrap();
14108        // Only the second sketch block changes.
14109        assert_eq!(
14110            src_delta.text.as_str(),
14111            "\
14112// Cube that requires the engine.
14113width = 2
14114sketch001 = startSketchOn(XY)
14115profile001 = startProfile(sketch001, at = [0, 0])
14116  |> yLine(length = width, tag = $seg1)
14117  |> xLine(length = width)
14118  |> yLine(length = -width)
14119  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14120  |> close()
14121extrude001 = extrude(profile001, length = width)
14122
14123// Get a value that requires the engine.
14124x = segLen(seg1)
14125
14126// Triangle with side length 2*x.
14127sketch(on = XY) {
14128  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14129  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14130  coincident([line1.end, line2.start])
14131  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14132  coincident([line2.end, line3.start])
14133  coincident([line3.end, line1.start])
14134  equalLength([line3, line1])
14135  equalLength([line1, line2])
14136  distance([line1.start, line1.end]) == 2 * x
14137}
14138
14139// Line segment with length x.
14140sketch2 = sketch(on = XY) {
14141  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14142  distance([line1.start, line1.end]) == x
14143}
14144"
14145        );
14146        let edited_sketch2_source = src_delta.text.clone();
14147
14148        // Execute mock to simulate drag end.
14149        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14150        assert_eq!(src_delta.text, edited_sketch2_source);
14151
14152        ctx.close().await;
14153        mock_ctx.close().await;
14154    }
14155
14156    #[tokio::test(flavor = "multi_thread")]
14157    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14158        clear_mem_cache().await;
14159
14160        let source = r#"sketch001 = sketch(on = XZ) {
14161  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14162}
14163sketch002 = sketch(on = XY) {
14164  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14165  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14166  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14167  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14168  coincident([line1.end, line2.start])
14169  coincident([line2.end, line3.start])
14170  coincident([line3.end, line4.start])
14171  coincident([line4.end, line1.start])
14172  parallel([line2, line4])
14173  parallel([line3, line1])
14174  perpendicular([line1, line2])
14175  horizontal(line3)
14176  coincident([line1.start, ORIGIN])
14177}
14178"#;
14179
14180        let program = Program::parse(source).unwrap().0.unwrap();
14181        let mut frontend = FrontendState::new();
14182        let ctx = ExecutorContext::new_with_engine(
14183            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
14184            Default::default(),
14185        );
14186        let mock_ctx = ExecutorContext::new_mock(None).await;
14187        let version = Version(0);
14188        let project_id = ProjectId(0);
14189        let file_id = FileId(0);
14190
14191        frontend.hack_set_program(&ctx, program).await.unwrap();
14192        let sketch_objects = frontend
14193            .scene_graph
14194            .objects
14195            .iter()
14196            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14197            .collect::<Vec<_>>();
14198        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14199
14200        let sketch1_id = sketch_objects[0].id;
14201        let sketch2_id = sketch_objects[1].id;
14202
14203        frontend
14204            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14205            .await
14206            .unwrap();
14207        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14208
14209        let scene_delta = frontend
14210            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14211            .await
14212            .unwrap();
14213        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14214
14215        clear_mem_cache().await;
14216        ctx.close().await;
14217        mock_ctx.close().await;
14218    }
14219
14220    // Regression tests: operations on source code with extra whitespace/newlines.
14221    // These test that NodePath-based lookups work correctly when source ranges
14222    // are shifted by extra whitespace that wouldn't be present after formatting.
14223
14224    #[tokio::test(flavor = "multi_thread")]
14225    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14226        // Extra newlines after @settings line - this shifts all source ranges.
14227        let initial_source = "@settings(defaultLengthUnit = mm)
14228
14229
14230
14231sketch001 = sketch(on = XY) {
14232  point(at = [1in, 2in])
14233}
14234";
14235
14236        let program = Program::parse(initial_source).unwrap().0.unwrap();
14237        let mut frontend = FrontendState::new();
14238
14239        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14240        let mock_ctx = ExecutorContext::new_mock(None).await;
14241        let version = Version(0);
14242        let project_id = ProjectId(0);
14243        let file_id = FileId(0);
14244
14245        frontend.hack_set_program(&ctx, program).await.unwrap();
14246        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14247        let sketch_id = sketch_object.id;
14248
14249        // Edit sketch should succeed despite extra newlines.
14250        frontend
14251            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14252            .await
14253            .unwrap();
14254
14255        // Add a new point to the sketch.
14256        let point_ctor = PointCtor {
14257            position: Point2d {
14258                x: Expr::Number(Number {
14259                    value: 5.0,
14260                    units: NumericSuffix::Mm,
14261                }),
14262                y: Expr::Number(Number {
14263                    value: 6.0,
14264                    units: NumericSuffix::Mm,
14265                }),
14266            },
14267        };
14268        let segment = SegmentCtor::Point(point_ctor);
14269        let (src_delta, scene_delta) = frontend
14270            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14271            .await
14272            .unwrap();
14273        // After adding a point, the source should be reformatted with standard whitespace.
14274        assert!(
14275            src_delta.text.contains("point(at = [5mm, 6mm])"),
14276            "Expected new point in source, got: {}",
14277            src_delta.text
14278        );
14279        assert!(!scene_delta.new_objects.is_empty());
14280
14281        ctx.close().await;
14282        mock_ctx.close().await;
14283    }
14284
14285    #[tokio::test(flavor = "multi_thread")]
14286    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14287        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14288
14289        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14290        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14291
14292        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14293        assert!(
14294            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14295            "Expected experimental settings to be added to source"
14296        );
14297    }
14298
14299    #[tokio::test(flavor = "multi_thread")]
14300    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14301        // Extra newlines after @settings, with an empty sketch block.
14302        let initial_source = "@settings(defaultLengthUnit = mm)
14303
14304
14305
14306s = sketch(on = XY) {}
14307";
14308
14309        let program = Program::parse(initial_source).unwrap().0.unwrap();
14310        let mut frontend = FrontendState::new();
14311
14312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14313        let mock_ctx = ExecutorContext::new_mock(None).await;
14314        let version = Version(0);
14315
14316        frontend.hack_set_program(&ctx, program).await.unwrap();
14317        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14318        let sketch_id = sketch_object.id;
14319
14320        let line_ctor = LineCtor {
14321            start: Point2d {
14322                x: Expr::Number(Number {
14323                    value: 0.0,
14324                    units: NumericSuffix::Mm,
14325                }),
14326                y: Expr::Number(Number {
14327                    value: 0.0,
14328                    units: NumericSuffix::Mm,
14329                }),
14330            },
14331            end: Point2d {
14332                x: Expr::Number(Number {
14333                    value: 10.0,
14334                    units: NumericSuffix::Mm,
14335                }),
14336                y: Expr::Number(Number {
14337                    value: 10.0,
14338                    units: NumericSuffix::Mm,
14339                }),
14340            },
14341            construction: None,
14342        };
14343        let segment = SegmentCtor::Line(line_ctor);
14344        let (src_delta, scene_delta) = frontend
14345            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14346            .await
14347            .unwrap();
14348        assert!(
14349            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14350            "Expected line in source, got: {}",
14351            src_delta.text
14352        );
14353        // Line creates start point, end point, and line segment.
14354        assert_eq!(scene_delta.new_objects.len(), 3);
14355
14356        ctx.close().await;
14357        mock_ctx.close().await;
14358    }
14359
14360    #[tokio::test(flavor = "multi_thread")]
14361    async fn test_extra_newlines_between_operations_edit_line() {
14362        // Extra newlines between @settings and sketch, and inside the sketch block.
14363        let initial_source = "@settings(defaultLengthUnit = mm)
14364
14365
14366sketch001 = sketch(on = XY) {
14367
14368  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14369
14370}
14371";
14372
14373        let program = Program::parse(initial_source).unwrap().0.unwrap();
14374        let mut frontend = FrontendState::new();
14375
14376        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14377        let mock_ctx = ExecutorContext::new_mock(None).await;
14378        let version = Version(0);
14379        let project_id = ProjectId(0);
14380        let file_id = FileId(0);
14381
14382        frontend.hack_set_program(&ctx, program).await.unwrap();
14383        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14384        let sketch_id = sketch_object.id;
14385        let sketch = expect_sketch(sketch_object);
14386
14387        // Extract segment IDs before edit_sketch borrows frontend mutably.
14388        let line_id = sketch
14389            .segments
14390            .iter()
14391            .copied()
14392            .find(|seg_id| {
14393                matches!(
14394                    &frontend.scene_graph.objects[seg_id.0].kind,
14395                    ObjectKind::Segment {
14396                        segment: Segment::Line(_)
14397                    }
14398                )
14399            })
14400            .expect("Expected a line segment in sketch");
14401
14402        // Enter sketch edit mode.
14403        frontend
14404            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14405            .await
14406            .unwrap();
14407
14408        // Edit the line.
14409        let line_ctor = LineCtor {
14410            start: Point2d {
14411                x: Expr::Var(Number {
14412                    value: 1.0,
14413                    units: NumericSuffix::Mm,
14414                }),
14415                y: Expr::Var(Number {
14416                    value: 2.0,
14417                    units: NumericSuffix::Mm,
14418                }),
14419            },
14420            end: Point2d {
14421                x: Expr::Var(Number {
14422                    value: 13.0,
14423                    units: NumericSuffix::Mm,
14424                }),
14425                y: Expr::Var(Number {
14426                    value: 14.0,
14427                    units: NumericSuffix::Mm,
14428                }),
14429            },
14430            construction: None,
14431        };
14432        let segments = vec![ExistingSegmentCtor {
14433            id: line_id,
14434            ctor: SegmentCtor::Line(line_ctor),
14435        }];
14436        let (src_delta, _scene_delta) = frontend
14437            .edit_segments(&mock_ctx, version, sketch_id, segments)
14438            .await
14439            .unwrap();
14440        assert!(
14441            src_delta
14442                .text
14443                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14444            "Expected edited line in source, got: {}",
14445            src_delta.text
14446        );
14447
14448        ctx.close().await;
14449        mock_ctx.close().await;
14450    }
14451
14452    #[tokio::test(flavor = "multi_thread")]
14453    async fn test_extra_newlines_delete_segment() {
14454        // Extra whitespace before and after the sketch block.
14455        let initial_source = "@settings(defaultLengthUnit = mm)
14456
14457
14458
14459sketch001 = sketch(on = XY) {
14460  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14461}
14462";
14463
14464        let program = Program::parse(initial_source).unwrap().0.unwrap();
14465        let mut frontend = FrontendState::new();
14466
14467        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14468        let mock_ctx = ExecutorContext::new_mock(None).await;
14469        let version = Version(0);
14470
14471        frontend.hack_set_program(&ctx, program).await.unwrap();
14472        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14473        let sketch_id = sketch_object.id;
14474        let sketch = expect_sketch(sketch_object);
14475
14476        // The sketch should have 3 segments: start point, center point, and the circle.
14477        assert_eq!(sketch.segments.len(), 3);
14478        let circle_id = sketch.segments[2];
14479
14480        // Delete the circle despite extra newlines in original source.
14481        let (src_delta, scene_delta) = frontend
14482            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14483            .await
14484            .unwrap();
14485        assert!(
14486            src_delta.text.contains("sketch(on = XY) {"),
14487            "Expected sketch block in source, got: {}",
14488            src_delta.text
14489        );
14490        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14491        let new_sketch = expect_sketch(new_sketch_object);
14492        assert_eq!(new_sketch.segments.len(), 0);
14493
14494        ctx.close().await;
14495        mock_ctx.close().await;
14496    }
14497
14498    #[tokio::test(flavor = "multi_thread")]
14499    async fn test_unformatted_source_add_arc() {
14500        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14501        let initial_source = "@settings(defaultLengthUnit = mm)
14502
14503
14504
14505
14506sketch001 = sketch(on = XY) {
14507}
14508";
14509
14510        let program = Program::parse(initial_source).unwrap().0.unwrap();
14511        let mut frontend = FrontendState::new();
14512
14513        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14514        let mock_ctx = ExecutorContext::new_mock(None).await;
14515        let version = Version(0);
14516
14517        frontend.hack_set_program(&ctx, program).await.unwrap();
14518        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14519        let sketch_id = sketch_object.id;
14520
14521        let arc_ctor = ArcCtor {
14522            start: Point2d {
14523                x: Expr::Var(Number {
14524                    value: 5.0,
14525                    units: NumericSuffix::Mm,
14526                }),
14527                y: Expr::Var(Number {
14528                    value: 0.0,
14529                    units: NumericSuffix::Mm,
14530                }),
14531            },
14532            end: Point2d {
14533                x: Expr::Var(Number {
14534                    value: 0.0,
14535                    units: NumericSuffix::Mm,
14536                }),
14537                y: Expr::Var(Number {
14538                    value: 5.0,
14539                    units: NumericSuffix::Mm,
14540                }),
14541            },
14542            center: Point2d {
14543                x: Expr::Var(Number {
14544                    value: 0.0,
14545                    units: NumericSuffix::Mm,
14546                }),
14547                y: Expr::Var(Number {
14548                    value: 0.0,
14549                    units: NumericSuffix::Mm,
14550                }),
14551            },
14552            construction: None,
14553        };
14554        let segment = SegmentCtor::Arc(arc_ctor);
14555        let (src_delta, scene_delta) = frontend
14556            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14557            .await
14558            .unwrap();
14559        assert!(
14560            src_delta
14561                .text
14562                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14563            "Expected arc in source, got: {}",
14564            src_delta.text
14565        );
14566        assert!(!scene_delta.new_objects.is_empty());
14567
14568        ctx.close().await;
14569        mock_ctx.close().await;
14570    }
14571
14572    #[tokio::test(flavor = "multi_thread")]
14573    async fn test_extra_newlines_add_circle() {
14574        // Extra blank lines between settings and sketch.
14575        let initial_source = "@settings(defaultLengthUnit = mm)
14576
14577
14578
14579sketch001 = sketch(on = XY) {
14580}
14581";
14582
14583        let program = Program::parse(initial_source).unwrap().0.unwrap();
14584        let mut frontend = FrontendState::new();
14585
14586        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14587        let mock_ctx = ExecutorContext::new_mock(None).await;
14588        let version = Version(0);
14589
14590        frontend.hack_set_program(&ctx, program).await.unwrap();
14591        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14592        let sketch_id = sketch_object.id;
14593
14594        let circle_ctor = CircleCtor {
14595            start: Point2d {
14596                x: Expr::Var(Number {
14597                    value: 5.0,
14598                    units: NumericSuffix::Mm,
14599                }),
14600                y: Expr::Var(Number {
14601                    value: 0.0,
14602                    units: NumericSuffix::Mm,
14603                }),
14604            },
14605            center: Point2d {
14606                x: Expr::Var(Number {
14607                    value: 0.0,
14608                    units: NumericSuffix::Mm,
14609                }),
14610                y: Expr::Var(Number {
14611                    value: 0.0,
14612                    units: NumericSuffix::Mm,
14613                }),
14614            },
14615            construction: None,
14616        };
14617        let segment = SegmentCtor::Circle(circle_ctor);
14618        let (src_delta, scene_delta) = frontend
14619            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14620            .await
14621            .unwrap();
14622        assert!(
14623            src_delta
14624                .text
14625                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14626            "Expected circle in source, got: {}",
14627            src_delta.text
14628        );
14629        assert!(!scene_delta.new_objects.is_empty());
14630
14631        ctx.close().await;
14632        mock_ctx.close().await;
14633    }
14634
14635    #[tokio::test(flavor = "multi_thread")]
14636    async fn test_extra_newlines_add_constraint() {
14637        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14638        let initial_source = "@settings(defaultLengthUnit = mm)
14639
14640
14641
14642sketch001 = sketch(on = XY) {
14643  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14644  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14645}
14646";
14647
14648        let program = Program::parse(initial_source).unwrap().0.unwrap();
14649        let mut frontend = FrontendState::new();
14650
14651        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14652        let mock_ctx = ExecutorContext::new_mock(None).await;
14653        let version = Version(0);
14654        let project_id = ProjectId(0);
14655        let file_id = FileId(0);
14656
14657        frontend.hack_set_program(&ctx, program).await.unwrap();
14658        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14659        let sketch_id = sketch_object.id;
14660        let sketch = expect_sketch(sketch_object);
14661
14662        // Extract segment data before edit_sketch borrows frontend mutably.
14663        let line_ids: Vec<ObjectId> = sketch
14664            .segments
14665            .iter()
14666            .copied()
14667            .filter(|seg_id| {
14668                matches!(
14669                    &frontend.scene_graph.objects[seg_id.0].kind,
14670                    ObjectKind::Segment {
14671                        segment: Segment::Line(_)
14672                    }
14673                )
14674            })
14675            .collect();
14676        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14677
14678        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14679        let ObjectKind::Segment {
14680            segment: Segment::Line(line1_data),
14681        } = &line1.kind
14682        else {
14683            panic!("Expected line");
14684        };
14685        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14686        let ObjectKind::Segment {
14687            segment: Segment::Line(line2_data),
14688        } = &line2.kind
14689        else {
14690            panic!("Expected line");
14691        };
14692
14693        // Build constraint before entering sketch mode.
14694        let constraint = Constraint::Coincident(Coincident {
14695            segments: vec![line1_data.end.into(), line2_data.start.into()],
14696        });
14697
14698        // Enter sketch edit mode.
14699        frontend
14700            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14701            .await
14702            .unwrap();
14703        let (src_delta, _scene_delta) = frontend
14704            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14705            .await
14706            .unwrap();
14707        assert!(
14708            src_delta.text.contains("coincident("),
14709            "Expected coincident constraint in source, got: {}",
14710            src_delta.text
14711        );
14712
14713        ctx.close().await;
14714        mock_ctx.close().await;
14715    }
14716
14717    #[tokio::test(flavor = "multi_thread")]
14718    async fn test_extra_newlines_add_line_then_edit_line() {
14719        // Extra newlines after @settings - add a line, then edit it.
14720        let initial_source = "@settings(defaultLengthUnit = mm)
14721
14722
14723
14724sketch001 = sketch(on = XY) {
14725}
14726";
14727
14728        let program = Program::parse(initial_source).unwrap().0.unwrap();
14729        let mut frontend = FrontendState::new();
14730
14731        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14732        let mock_ctx = ExecutorContext::new_mock(None).await;
14733        let version = Version(0);
14734
14735        frontend.hack_set_program(&ctx, program).await.unwrap();
14736        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14737        let sketch_id = sketch_object.id;
14738
14739        // Add a line.
14740        let line_ctor = LineCtor {
14741            start: Point2d {
14742                x: Expr::Number(Number {
14743                    value: 0.0,
14744                    units: NumericSuffix::Mm,
14745                }),
14746                y: Expr::Number(Number {
14747                    value: 0.0,
14748                    units: NumericSuffix::Mm,
14749                }),
14750            },
14751            end: Point2d {
14752                x: Expr::Number(Number {
14753                    value: 10.0,
14754                    units: NumericSuffix::Mm,
14755                }),
14756                y: Expr::Number(Number {
14757                    value: 10.0,
14758                    units: NumericSuffix::Mm,
14759                }),
14760            },
14761            construction: None,
14762        };
14763        let segment = SegmentCtor::Line(line_ctor);
14764        let (src_delta, scene_delta) = frontend
14765            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14766            .await
14767            .unwrap();
14768        assert!(
14769            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14770            "Expected line in source after add, got: {}",
14771            src_delta.text
14772        );
14773        // Line creates start point, end point, and line segment.
14774        let line_id = *scene_delta.new_objects.last().unwrap();
14775
14776        // Edit the line.
14777        let line_ctor = LineCtor {
14778            start: Point2d {
14779                x: Expr::Number(Number {
14780                    value: 1.0,
14781                    units: NumericSuffix::Mm,
14782                }),
14783                y: Expr::Number(Number {
14784                    value: 2.0,
14785                    units: NumericSuffix::Mm,
14786                }),
14787            },
14788            end: Point2d {
14789                x: Expr::Number(Number {
14790                    value: 13.0,
14791                    units: NumericSuffix::Mm,
14792                }),
14793                y: Expr::Number(Number {
14794                    value: 14.0,
14795                    units: NumericSuffix::Mm,
14796                }),
14797            },
14798            construction: None,
14799        };
14800        let segments = vec![ExistingSegmentCtor {
14801            id: line_id,
14802            ctor: SegmentCtor::Line(line_ctor),
14803        }];
14804        let (src_delta, scene_delta) = frontend
14805            .edit_segments(&mock_ctx, version, sketch_id, segments)
14806            .await
14807            .unwrap();
14808        assert!(
14809            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14810            "Expected edited line in source, got: {}",
14811            src_delta.text
14812        );
14813        assert_eq!(scene_delta.new_objects, vec![]);
14814
14815        ctx.close().await;
14816        mock_ctx.close().await;
14817    }
14818}