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::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::Expr;
53use crate::frontend::api::FileId;
54use crate::frontend::api::Number;
55use crate::frontend::api::ObjectId;
56use crate::frontend::api::ObjectKind;
57use crate::frontend::api::Plane;
58use crate::frontend::api::ProjectId;
59use crate::frontend::api::RestoreSketchCheckpointOutcome;
60use crate::frontend::api::SceneGraph;
61use crate::frontend::api::SceneGraphDelta;
62use crate::frontend::api::SketchCheckpointId;
63use crate::frontend::api::SourceDelta;
64use crate::frontend::api::SourceRef;
65use crate::frontend::api::Version;
66use crate::frontend::modify::find_defined_names;
67use crate::frontend::modify::next_free_name;
68use crate::frontend::modify::next_free_name_with_padding;
69use crate::frontend::sketch::Coincident;
70use crate::frontend::sketch::Constraint;
71use crate::frontend::sketch::ConstraintSegment;
72use crate::frontend::sketch::Diameter;
73use crate::frontend::sketch::ExistingSegmentCtor;
74use crate::frontend::sketch::Horizontal;
75use crate::frontend::sketch::LineCtor;
76use crate::frontend::sketch::Point2d;
77use crate::frontend::sketch::Radius;
78use crate::frontend::sketch::Segment;
79use crate::frontend::sketch::SegmentCtor;
80use crate::frontend::sketch::SketchApi;
81use crate::frontend::sketch::SketchCtor;
82use crate::frontend::sketch::Vertical;
83use crate::frontend::traverse::MutateBodyItem;
84use crate::frontend::traverse::TraversalReturn;
85use crate::frontend::traverse::Visitor;
86use crate::frontend::traverse::dfs_mut;
87use crate::id::IncIdGenerator;
88use crate::parsing::ast::types as ast;
89use crate::pretty::NumericSuffix;
90use crate::std::constraints::LinesAtAngleKind;
91use crate::walk::NodeMut;
92use crate::walk::Visitable;
93
94pub(crate) mod api;
95pub(crate) mod modify;
96pub(crate) mod sketch;
97
98pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
99
100#[derive(Debug, Clone)]
101struct SketchCheckpoint {
102    id: SketchCheckpointId,
103    source: SourceDelta,
104    program: Program,
105    scene_graph: SceneGraph,
106    exec_outcome: ExecOutcome,
107    point_freedom_cache: HashMap<ObjectId, Freedom>,
108    mock_memory: Option<SketchModeState>,
109}
110mod traverse;
111pub(crate) mod trim;
112
113struct ArcSizeConstraintParams {
114    points: Vec<ObjectId>,
115    function_name: &'static str,
116    value: f64,
117    units: NumericSuffix,
118    label_position: Option<Point2d<Number>>,
119    constraint_type_name: &'static str,
120}
121
122const POINT_FN: &str = "point";
123const POINT_AT_PARAM: &str = "at";
124const LINE_FN: &str = "line";
125const LINE_VARIABLE: &str = "line";
126const LINE_START_PARAM: &str = "start";
127const LINE_END_PARAM: &str = "end";
128const ARC_FN: &str = "arc";
129const ARC_VARIABLE: &str = "arc";
130const ARC_START_PARAM: &str = "start";
131const ARC_END_PARAM: &str = "end";
132const ARC_CENTER_PARAM: &str = "center";
133const CIRCLE_FN: &str = "circle";
134const CIRCLE_VARIABLE: &str = "circle";
135const CIRCLE_START_PARAM: &str = "start";
136const CIRCLE_CENTER_PARAM: &str = "center";
137const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
138const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
139const LABEL_POSITION_PARAM: &str = "labelPosition";
140
141const COINCIDENT_FN: &str = "coincident";
142const DIAMETER_FN: &str = "diameter";
143const DISTANCE_FN: &str = "distance";
144const FIXED_FN: &str = "fixed";
145const ANGLE_FN: &str = "angle";
146const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
147const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
148const EQUAL_LENGTH_FN: &str = "equalLength";
149const EQUAL_RADIUS_FN: &str = "equalRadius";
150const HORIZONTAL_FN: &str = "horizontal";
151const MIDPOINT_FN: &str = "midpoint";
152const MIDPOINT_POINT_PARAM: &str = "point";
153const RADIUS_FN: &str = "radius";
154const SYMMETRIC_FN: &str = "symmetric";
155const SYMMETRIC_AXIS_PARAM: &str = "axis";
156const TANGENT_FN: &str = "tangent";
157const VERTICAL_FN: &str = "vertical";
158
159const LINE_PROPERTY_START: &str = "start";
160const LINE_PROPERTY_END: &str = "end";
161
162const ARC_PROPERTY_START: &str = "start";
163const ARC_PROPERTY_END: &str = "end";
164const ARC_PROPERTY_CENTER: &str = "center";
165const CIRCLE_PROPERTY_START: &str = "start";
166const CIRCLE_PROPERTY_CENTER: &str = "center";
167const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
168const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
169
170const CONSTRUCTION_PARAM: &str = "construction";
171
172#[derive(Debug, Clone, Copy)]
173enum EditDeleteKind {
174    Edit,
175    DeleteNonSketch,
176}
177
178/// Options that control how an edit is re-executed and written back.
179struct ExecuteAfterEditOptions {
180    segment_ids_edited: AhashIndexSet<ObjectId>,
181    edit_kind: EditDeleteKind,
182    commit_solved_initial_guesses: bool,
183}
184
185impl EditDeleteKind {
186    /// Returns true if this edit is any type of deletion.
187    fn is_delete(&self) -> bool {
188        match self {
189            EditDeleteKind::Edit => false,
190            EditDeleteKind::DeleteNonSketch => true,
191        }
192    }
193
194    fn to_change_kind(self) -> ChangeKind {
195        match self {
196            EditDeleteKind::Edit => ChangeKind::Edit,
197            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
198        }
199    }
200}
201
202#[derive(Debug, Clone, Copy)]
203enum ChangeKind {
204    Add,
205    Edit,
206    Delete,
207    None,
208}
209
210#[derive(Debug, Clone, Serialize, ts_rs::TS)]
211#[ts(export, export_to = "FrontendApi.ts")]
212#[serde(tag = "type")]
213pub enum SetProgramOutcome {
214    #[serde(rename_all = "camelCase")]
215    Success {
216        scene_graph: Box<SceneGraph>,
217        exec_outcome: Box<ExecOutcome>,
218        checkpoint_id: Option<SketchCheckpointId>,
219    },
220    #[serde(rename_all = "camelCase")]
221    ExecFailure { error: Box<KclErrorWithOutputs> },
222}
223
224/// Options for a sketch segment edit that participates in drag solving.
225pub struct EditSegmentsOptions {
226    /// Narrows which edited scene objects receive temporary fixed constraints.
227    ///
228    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
229    /// disables those fixed constraints, which is useful for semantic edits such
230    /// as toggling construction state.
231    pub anchor_segment_ids: Option<Vec<ObjectId>>,
232    /// Hidden fixed cursor points that the referenced segment bodies must pass
233    /// through during solve.
234    pub drag_anchors: Vec<SegmentDragAnchor>,
235    /// Whether solver-updated initial guesses should be written back to KCL.
236    pub commit_solved_initial_guesses: bool,
237}
238
239/// Options for a distance-constraint label edit during sketch dragging.
240pub struct EditDistanceConstraintLabelPositionOptions {
241    /// Edited scene objects to keep anchored while previewing the label edit.
242    pub anchor_segment_ids: Vec<ObjectId>,
243    /// Whether solver-updated initial guesses should be written back to KCL.
244    pub commit_solved_initial_guesses: bool,
245}
246
247#[derive(Debug, Clone)]
248pub struct FrontendState {
249    program: Program,
250    scene_graph: SceneGraph,
251    /// Stores the last known freedom value for each point object.
252    /// This allows us to preserve freedom values when freedom analysis isn't run.
253    point_freedom_cache: HashMap<ObjectId, Freedom>,
254    /// One-shot drag anchors for the next segment edit. These ids define which
255    /// edited points/segments become temporary fixed constraints during solve.
256    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
257    /// One-shot segment-body drag anchors for the next segment edit. These add
258    /// a temporary solver point on the dragged segment that follows the cursor.
259    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
260    /// One-shot override for whether the next edit commits solver-updated
261    /// initial guesses back into KCL. Drag previews keep this off so only the
262    /// explicit drag edit feeds the next solve.
263    next_edit_commits_solver_solutions: Option<bool>,
264    sketch_checkpoints: VecDeque<SketchCheckpoint>,
265    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
266}
267
268impl Default for FrontendState {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl FrontendState {
275    pub fn new() -> Self {
276        Self {
277            program: Program::empty(),
278            scene_graph: SceneGraph {
279                project: ProjectId(0),
280                file: FileId(0),
281                version: Version(0),
282                objects: Default::default(),
283                settings: Default::default(),
284                sketch_mode: Default::default(),
285            },
286            point_freedom_cache: HashMap::new(),
287            next_drag_anchor_segment_ids: None,
288            next_segment_drag_anchors: None,
289            next_edit_commits_solver_solutions: None,
290            sketch_checkpoints: VecDeque::new(),
291            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
292        }
293    }
294
295    /// Get a reference to the scene graph
296    pub fn scene_graph(&self) -> &SceneGraph {
297        &self.scene_graph
298    }
299
300    pub fn default_length_unit(&self) -> UnitLength {
301        self.program
302            .meta_settings()
303            .ok()
304            .flatten()
305            .map(|settings| settings.default_length_units)
306            .unwrap_or(UnitLength::Millimeters)
307    }
308
309    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
310        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
311
312        let checkpoint = SketchCheckpoint {
313            id: checkpoint_id,
314            source: SourceDelta {
315                text: source_from_ast(&self.program.ast),
316            },
317            program: self.program.clone(),
318            scene_graph: self.scene_graph.clone(),
319            exec_outcome,
320            point_freedom_cache: self.point_freedom_cache.clone(),
321            mock_memory: read_old_memory().await,
322        };
323
324        self.sketch_checkpoints.push_back(checkpoint);
325        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
326            self.sketch_checkpoints.pop_front();
327        }
328
329        Ok(checkpoint_id)
330    }
331
332    /// Edit sketch segments with optional drag-solve overrides.
333    ///
334    /// Drag anchors add hidden fixed cursor points and constrain the referenced
335    /// segment bodies to pass through them, which lets body drags use the same
336    /// anchor model without pinning all child points. Preview callers disable
337    /// solver writeback so solved geometry can be returned without feeding every
338    /// solver value back into KCL.
339    pub async fn edit_segments_with_options(
340        &mut self,
341        ctx: &ExecutorContext,
342        version: Version,
343        sketch: ObjectId,
344        segments: Vec<ExistingSegmentCtor>,
345        options: EditSegmentsOptions,
346    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
347        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
348            self.next_drag_anchor_segment_ids
349                .replace(anchor_ids.into_iter().collect())
350        });
351        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
352        let previous_commit_mode = self
353            .next_edit_commits_solver_solutions
354            .replace(options.commit_solved_initial_guesses);
355        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
356        if let Some(previous_anchor_ids) = previous_anchor_ids {
357            self.next_drag_anchor_segment_ids = previous_anchor_ids;
358        }
359        self.next_segment_drag_anchors = previous_drag_anchors;
360        self.next_edit_commits_solver_solutions = previous_commit_mode;
361        result
362    }
363
364    /// Edit a distance-constraint label position with optional solver writeback.
365    ///
366    /// Drag previews set `commit_solved_initial_guesses` to false so label
367    /// placement can be previewed against solved geometry without advancing
368    /// persistent KCL state until drag completion.
369    pub async fn edit_distance_constraint_label_position_with_options(
370        &mut self,
371        ctx: &ExecutorContext,
372        version: Version,
373        sketch: ObjectId,
374        constraint_id: ObjectId,
375        label_position: Point2d<Number>,
376        options: EditDistanceConstraintLabelPositionOptions,
377    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
378        let previous_commit_mode = self
379            .next_edit_commits_solver_solutions
380            .replace(options.commit_solved_initial_guesses);
381        let result = SketchApi::edit_distance_constraint_label_position(
382            self,
383            ctx,
384            version,
385            sketch,
386            constraint_id,
387            label_position,
388            options.anchor_segment_ids,
389        )
390        .await;
391        self.next_edit_commits_solver_solutions = previous_commit_mode;
392        result
393    }
394
395    pub async fn restore_sketch_checkpoint(
396        &mut self,
397        checkpoint_id: SketchCheckpointId,
398    ) -> api::Result<RestoreSketchCheckpointOutcome> {
399        let checkpoint = self
400            .sketch_checkpoints
401            .iter()
402            .find(|checkpoint| checkpoint.id == checkpoint_id)
403            .cloned()
404            .ok_or_else(|| Error {
405                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
406            })?;
407
408        self.program = checkpoint.program;
409        self.scene_graph = checkpoint.scene_graph.clone();
410        self.point_freedom_cache = checkpoint.point_freedom_cache;
411        self.next_drag_anchor_segment_ids = None;
412        self.next_segment_drag_anchors = None;
413        self.next_edit_commits_solver_solutions = None;
414
415        if let Some(mock_memory) = checkpoint.mock_memory {
416            write_old_memory(mock_memory).await;
417        } else {
418            clear_mem_cache().await;
419        }
420
421        Ok(RestoreSketchCheckpointOutcome {
422            source_delta: checkpoint.source,
423            scene_graph_delta: SceneGraphDelta {
424                new_graph: self.scene_graph_for_ui(),
425                new_objects: Vec::new(),
426                invalidates_ids: true,
427                exec_outcome: checkpoint.exec_outcome,
428            },
429        })
430    }
431
432    pub fn clear_sketch_checkpoints(&mut self) {
433        self.sketch_checkpoints.clear();
434    }
435    fn scene_graph_for_ui(&self) -> SceneGraph {
436        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
437            matches!(
438                object.kind,
439                ObjectKind::Segment {
440                    segment: Segment::ControlPointSpline(_)
441                }
442            )
443        });
444
445        if !has_control_point_splines {
446            return self.scene_graph.clone();
447        }
448
449        let hidden_constraint_ids = self
450            .scene_graph
451            .objects
452            .iter()
453            .filter_map(|object| match &object.kind {
454                ObjectKind::Constraint {
455                    constraint: Constraint::Coincident(coincident),
456                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
457                    Some(object.id)
458                }
459                _ => None,
460            })
461            .collect::<HashSet<_>>();
462
463        if hidden_constraint_ids.is_empty() {
464            return self.scene_graph.clone();
465        }
466
467        let mut scene_graph = self.scene_graph.clone();
468        for object in &mut scene_graph.objects {
469            match &mut object.kind {
470                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
471                    object.kind = ObjectKind::Nil;
472                }
473                ObjectKind::Sketch(sketch) => {
474                    sketch
475                        .constraints
476                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
477                }
478                _ => {}
479            }
480        }
481
482        scene_graph
483    }
484}
485
486fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
487    let mut first_owner_id = None;
488    for segment_id in coincident.segment_ids() {
489        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
490            return false;
491        };
492
493        match first_owner_id {
494            Some(first_owner_id) if first_owner_id != owner_id => return false,
495            Some(_) => {}
496            None => first_owner_id = Some(owner_id),
497        }
498    }
499
500    first_owner_id.is_some()
501}
502
503fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
504    let object = scene_graph.objects.get(segment_id.0)?;
505    let ObjectKind::Segment { segment } = &object.kind else {
506        return None;
507    };
508
509    match segment {
510        Segment::ControlPointSpline(_) => Some(segment_id),
511        Segment::Point(point) => point
512            .owner
513            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
514        Segment::Line(line) => line
515            .owner
516            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
517        _ => None,
518    }
519}
520
521fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
522    matches!(
523        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
524        Some(ObjectKind::Segment {
525            segment: Segment::ControlPointSpline(_)
526        })
527    )
528}
529
530fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
531    let experimental_features_allowed = program
532        .meta_settings()
533        .ok()
534        .flatten()
535        .map(|settings| settings.experimental_features == WarningLevel::Allow)
536        .unwrap_or(false);
537    if experimental_features_allowed {
538        return Ok(program.clone());
539    }
540
541    program.change_experimental_features(Some(WarningLevel::Allow))
542}
543
544impl SketchApi for FrontendState {
545    async fn execute_mock(
546        &mut self,
547        ctx: &ExecutorContext,
548        _version: Version,
549        sketch: ObjectId,
550    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
551        let sketch_block_ref =
552            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
553
554        let mut truncated_program = self.program.clone();
555        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
556            .map_err(KclErrorWithOutputs::no_outputs)?;
557
558        // Execute.
559        let outcome = ctx
560            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
561            .await?;
562        let new_source = source_from_ast(&self.program.ast);
563        let src_delta = SourceDelta { text: new_source };
564        // MockConfig::default() has freedom_analysis: true
565        let outcome = self.update_state_after_exec(outcome, true);
566        let scene_graph_delta = SceneGraphDelta {
567            new_graph: self.scene_graph.clone(),
568            new_objects: Default::default(),
569            invalidates_ids: false,
570            exec_outcome: outcome,
571        };
572        Ok((src_delta, scene_graph_delta))
573    }
574
575    async fn new_sketch(
576        &mut self,
577        ctx: &ExecutorContext,
578        _project: ProjectId,
579        _file: FileId,
580        _version: Version,
581        args: SketchCtor,
582    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
583        // TODO: Check version.
584
585        let mut new_ast = self.program.ast.clone();
586        // Create updated KCL source from args.
587        let mut plane_ast =
588            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
589        let mut defined_names = find_defined_names(&new_ast);
590        let is_face_of_expr = matches!(
591            &plane_ast,
592            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
593        );
594        if is_face_of_expr {
595            let face_name = next_free_name_with_padding("face", &defined_names)
596                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
597            let face_decl = ast::VariableDeclaration::new(
598                ast::VariableDeclarator::new(&face_name, plane_ast),
599                ast::ItemVisibility::Default,
600                ast::VariableKind::Const,
601            );
602            new_ast
603                .body
604                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
605                    face_decl,
606                ))));
607            defined_names.insert(face_name.clone());
608            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
609        }
610        let sketch_ast = ast::SketchBlock {
611            arguments: vec![ast::LabeledArg {
612                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
613                arg: plane_ast,
614            }],
615            body: Default::default(),
616            is_being_edited: false,
617            non_code_meta: Default::default(),
618            digest: None,
619        };
620        // Add a sketch block as a variable declaration directly, avoiding
621        // source-range mutation on a no-src node.
622        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
623            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
624        let sketch_decl = ast::VariableDeclaration::new(
625            ast::VariableDeclarator::new(
626                &sketch_name,
627                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
628            ),
629            ast::ItemVisibility::Default,
630            ast::VariableKind::Const,
631        );
632        new_ast
633            .body
634            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
635                sketch_decl,
636            ))));
637        // Convert to string source to create real source ranges.
638        let new_source = source_from_ast(&new_ast);
639        // Parse the new source.
640        let (new_program, errors) = Program::parse(&new_source)
641            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
642        if !errors.is_empty() {
643            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
644                "Error parsing KCL source after adding sketch: {errors:?}"
645            ))));
646        }
647        let Some(new_program) = new_program else {
648            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
649                "No AST produced after adding sketch".to_owned(),
650            )));
651        };
652
653        // Make sure to only set this if there are no errors.
654        self.program = new_program.clone();
655
656        // We need to do an engine execute so that the plane object gets created
657        // and is cached.
658        let outcome = ctx.run_with_caching(new_program.clone()).await?;
659        let freedom_analysis_ran = true;
660
661        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
662
663        let Some(sketch_id) = self
664            .scene_graph
665            .objects
666            .iter()
667            .filter_map(|object| match object.kind {
668                ObjectKind::Sketch(_) => Some(object.id),
669                _ => None,
670            })
671            .max_by_key(|id| id.0)
672        else {
673            return Err(KclErrorWithOutputs::from_error_outcome(
674                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
675                outcome,
676            ));
677        };
678        // Store the object in the scene.
679        self.scene_graph.sketch_mode = Some(sketch_id);
680
681        let src_delta = SourceDelta { text: new_source };
682        let scene_graph_delta = SceneGraphDelta {
683            new_graph: self.scene_graph_for_ui(),
684            invalidates_ids: false,
685            new_objects: vec![sketch_id],
686            exec_outcome: outcome,
687        };
688        Ok((src_delta, scene_graph_delta, sketch_id))
689    }
690
691    async fn edit_sketch(
692        &mut self,
693        ctx: &ExecutorContext,
694        _project: ProjectId,
695        _file: FileId,
696        _version: Version,
697        sketch: ObjectId,
698    ) -> ExecResult<SceneGraphDelta> {
699        // TODO: Check version.
700
701        // Look up existing sketch.
702        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
703            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
704        })?;
705        let ObjectKind::Sketch(_) = &sketch_object.kind else {
706            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
707                "Object is not a sketch, it is {}",
708                sketch_object.kind.human_friendly_kind_with_article()
709            ))));
710        };
711        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
712
713        // Enter sketch mode by setting the sketch_mode.
714        self.scene_graph.sketch_mode = Some(sketch);
715
716        // Truncate after the sketch block for mock execution.
717        let mut truncated_program = self.program.clone();
718        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
719            .map_err(KclErrorWithOutputs::no_outputs)?;
720
721        // Execute in mock mode to ensure state is up to date. The caller will
722        // want freedom analysis to display segments correctly.
723        let outcome = ctx
724            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
725            .await?;
726
727        // MockConfig::default() has freedom_analysis: true
728        let outcome = self.update_state_after_exec(outcome, true);
729        let scene_graph_delta = SceneGraphDelta {
730            new_graph: self.scene_graph_for_ui(),
731            invalidates_ids: false,
732            new_objects: Vec::new(),
733            exec_outcome: outcome,
734        };
735        Ok(scene_graph_delta)
736    }
737
738    async fn exit_sketch(
739        &mut self,
740        ctx: &ExecutorContext,
741        _version: Version,
742        sketch: ObjectId,
743    ) -> ExecResult<SceneGraph> {
744        // TODO: Check version.
745        #[cfg(not(target_arch = "wasm32"))]
746        let _ = sketch;
747        #[cfg(target_arch = "wasm32")]
748        if self.scene_graph.sketch_mode != Some(sketch) {
749            web_sys::console::warn_1(
750                &format!(
751                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
752                    &self.scene_graph.sketch_mode
753                )
754                .into(),
755            );
756        }
757        self.scene_graph.sketch_mode = None;
758
759        // Execute.
760        let outcome = ctx.run_with_caching(self.program.clone()).await?;
761
762        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
763        self.update_state_after_exec(outcome, false);
764
765        Ok(self.scene_graph_for_ui())
766    }
767
768    async fn delete_sketch(
769        &mut self,
770        ctx: &ExecutorContext,
771        _version: Version,
772        sketch: ObjectId,
773    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
774        // TODO: Check version.
775
776        let mut new_ast = self.program.ast.clone();
777
778        // Look up existing sketch.
779        let sketch_id = sketch;
780        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
781            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
782        })?;
783        let ObjectKind::Sketch(_) = &sketch_object.kind else {
784            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
785                "Object is not a sketch, it is {}",
786                sketch_object.kind.human_friendly_kind_with_article(),
787            ))));
788        };
789
790        // Modify the AST to remove the sketch.
791        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
792            .map_err(KclErrorWithOutputs::no_outputs)?;
793
794        self.execute_after_delete_sketch(ctx, &mut new_ast).await
795    }
796
797    async fn add_segment(
798        &mut self,
799        ctx: &ExecutorContext,
800        _version: Version,
801        sketch: ObjectId,
802        segment: SegmentCtor,
803        _label: Option<String>,
804    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
805        // TODO: Check version.
806        match segment {
807            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
808            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
809            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
810            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
811            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
812        }
813    }
814
815    async fn edit_segments(
816        &mut self,
817        ctx: &ExecutorContext,
818        _version: Version,
819        sketch: ObjectId,
820        segments: Vec<ExistingSegmentCtor>,
821    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
822        // TODO: Check version.
823        let sketch_block_ref =
824            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
825
826        let mut new_ast = self.program.ast.clone();
827        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
828        let mut invalidates_ids = false;
829
830        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
831        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
832        for segment in &segments {
833            edited_segment_ids.insert(segment.id);
834            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
835                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
836                && let ObjectKind::Segment {
837                    segment: Segment::ControlPointSpline(existing_spline),
838                } = &existing_object.kind
839                && existing_spline.controls.len() != new_ctor.points.len()
840            {
841                invalidates_ids = true;
842            }
843        }
844        let drag_anchor_segment_ids = self
845            .next_drag_anchor_segment_ids
846            .take()
847            .unwrap_or_else(|| edited_segment_ids.clone());
848        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
849
850        // Preprocess segments into a final_edits vector to handle if segments contains:
851        // - edit start point of line1 (as SegmentCtor::Point)
852        // - edit end point of line1 (as SegmentCtor::Point)
853        //
854        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
855        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
856        //
857        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
858        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
859        // so the above example would result in a single line1 edit:
860        // - the first start point edit creates a new line edit entry in final_edits
861        // - the second end point edit finds this line edit and mutates the end position only.
862        //
863        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
864        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
865
866        for segment in segments {
867            let segment_id = segment.id;
868            match segment.ctor {
869                SegmentCtor::Point(ctor) => {
870                    // Find the owner, if any (point -> line / arc)
871                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
872                        && let ObjectKind::Segment { segment } = &segment_object.kind
873                        && let Segment::Point(point) = segment
874                        && let Some(owner_id) = point.owner
875                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
876                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
877                    {
878                        match owner_segment {
879                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
880                                if let Some(existing) = final_edits.get_mut(&owner_id) {
881                                    let SegmentCtor::Line(line_ctor) = existing else {
882                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
883                                            "Internal: Expected line ctor for owner, but found {}",
884                                            existing.human_friendly_kind_with_article()
885                                        ))));
886                                    };
887                                    // Line owner is already in final_edits -> apply this point edit
888                                    if line.start == segment_id {
889                                        line_ctor.start = ctor.position;
890                                    } else {
891                                        line_ctor.end = ctor.position;
892                                    }
893                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
894                                    // Line owner is not in final_edits yet -> create it
895                                    let mut line_ctor = line_ctor.clone();
896                                    if line.start == segment_id {
897                                        line_ctor.start = ctor.position;
898                                    } else {
899                                        line_ctor.end = ctor.position;
900                                    }
901                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
902                                } else {
903                                    // This should never run..
904                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
905                                        "Internal: Line does not have line ctor, but found {}",
906                                        line.ctor.human_friendly_kind_with_article()
907                                    ))));
908                                }
909                                continue;
910                            }
911                            Segment::Arc(arc)
912                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
913                            {
914                                if let Some(existing) = final_edits.get_mut(&owner_id) {
915                                    let SegmentCtor::Arc(arc_ctor) = existing else {
916                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
917                                            "Internal: Expected arc ctor for owner, but found {}",
918                                            existing.human_friendly_kind_with_article()
919                                        ))));
920                                    };
921                                    if arc.start == segment_id {
922                                        arc_ctor.start = ctor.position;
923                                    } else if arc.end == segment_id {
924                                        arc_ctor.end = ctor.position;
925                                    } else {
926                                        arc_ctor.center = ctor.position;
927                                    }
928                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
929                                    let mut arc_ctor = arc_ctor.clone();
930                                    if arc.start == segment_id {
931                                        arc_ctor.start = ctor.position;
932                                    } else if arc.end == segment_id {
933                                        arc_ctor.end = ctor.position;
934                                    } else {
935                                        arc_ctor.center = ctor.position;
936                                    }
937                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
938                                } else {
939                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
940                                        "Internal: Arc does not have arc ctor, but found {}",
941                                        arc.ctor.human_friendly_kind_with_article()
942                                    ))));
943                                }
944                                continue;
945                            }
946                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
947                                if let Some(existing) = final_edits.get_mut(&owner_id) {
948                                    let SegmentCtor::Circle(circle_ctor) = existing else {
949                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
950                                            "Internal: Expected circle ctor for owner, but found {}",
951                                            existing.human_friendly_kind_with_article()
952                                        ))));
953                                    };
954                                    if circle.start == segment_id {
955                                        circle_ctor.start = ctor.position;
956                                    } else {
957                                        circle_ctor.center = ctor.position;
958                                    }
959                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
960                                    let mut circle_ctor = circle_ctor.clone();
961                                    if circle.start == segment_id {
962                                        circle_ctor.start = ctor.position;
963                                    } else {
964                                        circle_ctor.center = ctor.position;
965                                    }
966                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
967                                } else {
968                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
969                                        "Internal: Circle does not have circle ctor, but found {}",
970                                        circle.ctor.human_friendly_kind_with_article()
971                                    ))));
972                                }
973                                continue;
974                            }
975                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
976                                let Some(control_index) =
977                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
978                                else {
979                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
980                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
981                                    ))));
982                                };
983                                if let Some(existing) = final_edits.get_mut(&owner_id) {
984                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
985                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
986                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
987                                            existing.human_friendly_kind_with_article()
988                                        ))));
989                                    };
990                                    spline_ctor.points[control_index] = ctor.position;
991                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
992                                    let mut spline_ctor = spline_ctor.clone();
993                                    spline_ctor.points[control_index] = ctor.position;
994                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
995                                } else {
996                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
997                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
998                                        spline.ctor.human_friendly_kind_with_article()
999                                    ))));
1000                                }
1001                                continue;
1002                            }
1003                            _ => {}
1004                        }
1005                    }
1006
1007                    // No owner, it's an individual point
1008                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1009                }
1010                SegmentCtor::Line(ctor) => {
1011                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1012                }
1013                SegmentCtor::Arc(ctor) => {
1014                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1015                }
1016                SegmentCtor::Circle(ctor) => {
1017                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1018                }
1019                SegmentCtor::ControlPointSpline(ctor) => {
1020                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1021                }
1022            }
1023        }
1024
1025        for (segment_id, ctor) in final_edits {
1026            match ctor {
1027                SegmentCtor::Point(ctor) => self
1028                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1029                    .map_err(KclErrorWithOutputs::no_outputs)?,
1030                SegmentCtor::Line(ctor) => self
1031                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1032                    .map_err(KclErrorWithOutputs::no_outputs)?,
1033                SegmentCtor::Arc(ctor) => self
1034                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1035                    .map_err(KclErrorWithOutputs::no_outputs)?,
1036                SegmentCtor::Circle(ctor) => self
1037                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1038                    .map_err(KclErrorWithOutputs::no_outputs)?,
1039                SegmentCtor::ControlPointSpline(ctor) => self
1040                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1041                    .map_err(KclErrorWithOutputs::no_outputs)?,
1042            }
1043        }
1044        let (source_delta, mut scene_graph_delta) = self
1045            .execute_after_edit(
1046                ctx,
1047                sketch,
1048                sketch_block_ref,
1049                &mut new_ast,
1050                ExecuteAfterEditOptions {
1051                    segment_ids_edited: drag_anchor_segment_ids,
1052                    edit_kind: EditDeleteKind::Edit,
1053                    commit_solved_initial_guesses,
1054                },
1055            )
1056            .await?;
1057        if invalidates_ids {
1058            scene_graph_delta.invalidates_ids = true;
1059        }
1060        Ok((source_delta, scene_graph_delta))
1061    }
1062
1063    async fn delete_objects(
1064        &mut self,
1065        ctx: &ExecutorContext,
1066        _version: Version,
1067        sketch: ObjectId,
1068        constraint_ids: Vec<ObjectId>,
1069        segment_ids: Vec<ObjectId>,
1070    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1071        // TODO: Check version.
1072        let sketch_block_ref =
1073            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1074
1075        // Deduplicate IDs.
1076        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1077        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1078
1079        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1080        // also delete the point, as well as other points that are owned by the owner.
1081        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1082
1083        for segment_id in segment_ids_set.iter().copied() {
1084            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1085                let ObjectKind::Segment { segment } = &segment_object.kind else {
1086                    return None;
1087                };
1088                match segment {
1089                    Segment::Point(point) => point.owner,
1090                    Segment::Line(line) => line.owner,
1091                    _ => None,
1092                }
1093            });
1094
1095            if let Some(owner_id) = owner_id
1096                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1097                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1098                && matches!(
1099                    owner_segment,
1100                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1101                )
1102            {
1103                // segment is owned -> delete the owner
1104                resolved_segment_ids_to_delete.insert(owner_id);
1105            } else {
1106                // segment is not owned by anything -> can be deleted
1107                resolved_segment_ids_to_delete.insert(segment_id);
1108            }
1109        }
1110        let referenced_constraint_ids = self
1111            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1112            .map_err(KclErrorWithOutputs::no_outputs)?;
1113
1114        let mut new_ast = self.program.ast.clone();
1115
1116        for constraint_id in referenced_constraint_ids {
1117            if constraint_ids_set.contains(&constraint_id) {
1118                continue;
1119            }
1120
1121            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1122                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1123            })?;
1124            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1125                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1126                    "Object is not a constraint, it is {}",
1127                    constraint_object.kind.human_friendly_kind_with_article()
1128                ))));
1129            };
1130
1131            match constraint {
1132                Constraint::Coincident(coincident) => {
1133                    let remaining_segments =
1134                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1135
1136                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1137                    if remaining_segments.len() >= 2 {
1138                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1139                            .map_err(KclErrorWithOutputs::no_outputs)?;
1140                    } else {
1141                        constraint_ids_set.insert(constraint_id);
1142                    }
1143                }
1144                Constraint::EqualRadius(equal_radius) => {
1145                    let remaining_input = equal_radius
1146                        .input
1147                        .iter()
1148                        .copied()
1149                        .filter(|segment_id| {
1150                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1151                        })
1152                        .collect::<Vec<_>>();
1153
1154                    if remaining_input.len() >= 2 {
1155                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1156                            .map_err(KclErrorWithOutputs::no_outputs)?;
1157                    } else {
1158                        constraint_ids_set.insert(constraint_id);
1159                    }
1160                }
1161                Constraint::LinesEqualLength(lines_equal_length) => {
1162                    let remaining_lines = lines_equal_length
1163                        .lines
1164                        .iter()
1165                        .copied()
1166                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1167                        .collect::<Vec<_>>();
1168
1169                    // Equal length constraint is only valid with at least 2 lines
1170                    if remaining_lines.len() >= 2 {
1171                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1172                            .map_err(KclErrorWithOutputs::no_outputs)?;
1173                    } else {
1174                        constraint_ids_set.insert(constraint_id);
1175                    }
1176                }
1177                Constraint::Parallel(parallel) => {
1178                    let remaining_lines = parallel
1179                        .lines
1180                        .iter()
1181                        .copied()
1182                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1183                        .collect::<Vec<_>>();
1184
1185                    if remaining_lines.len() >= 2 {
1186                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1187                            .map_err(KclErrorWithOutputs::no_outputs)?;
1188                    } else {
1189                        constraint_ids_set.insert(constraint_id);
1190                    }
1191                }
1192                Constraint::Horizontal(Horizontal::Points { points }) => {
1193                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1194
1195                    if remaining_points.len() >= 2 {
1196                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1197                            .map_err(KclErrorWithOutputs::no_outputs)?;
1198                    } else {
1199                        constraint_ids_set.insert(constraint_id);
1200                    }
1201                }
1202                Constraint::Vertical(Vertical::Points { points }) => {
1203                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1204
1205                    if remaining_points.len() >= 2 {
1206                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1207                            .map_err(KclErrorWithOutputs::no_outputs)?;
1208                    } else {
1209                        constraint_ids_set.insert(constraint_id);
1210                    }
1211                }
1212                Constraint::Fixed(fixed) => {
1213                    if fixed.points.iter().any(|fixed_point| {
1214                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1215                    }) {
1216                        constraint_ids_set.insert(constraint_id);
1217                    }
1218                }
1219                _ => {
1220                    // All other constraint types: if referenced by a segment -> delete the constraint
1221                    constraint_ids_set.insert(constraint_id);
1222                }
1223            }
1224        }
1225
1226        for constraint_id in constraint_ids_set {
1227            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1228                .map_err(KclErrorWithOutputs::no_outputs)?;
1229        }
1230        for segment_id in resolved_segment_ids_to_delete {
1231            self.delete_segment(&mut new_ast, sketch, segment_id)
1232                .map_err(KclErrorWithOutputs::no_outputs)?;
1233        }
1234
1235        self.execute_after_edit(
1236            ctx,
1237            sketch,
1238            sketch_block_ref,
1239            &mut new_ast,
1240            ExecuteAfterEditOptions {
1241                segment_ids_edited: Default::default(),
1242                edit_kind: EditDeleteKind::DeleteNonSketch,
1243                commit_solved_initial_guesses: true,
1244            },
1245        )
1246        .await
1247    }
1248
1249    async fn add_constraint(
1250        &mut self,
1251        ctx: &ExecutorContext,
1252        _version: Version,
1253        sketch: ObjectId,
1254        constraint: Constraint,
1255    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1256        // TODO: Check version.
1257
1258        // Save the original state as a backup - we'll restore it if anything fails
1259        let original_program = self.program.clone();
1260        let original_scene_graph = self.scene_graph.clone();
1261
1262        let mut new_ast = self.program.ast.clone();
1263        let sketch_block_ref = match constraint {
1264            Constraint::Coincident(coincident) => self
1265                .add_coincident(sketch, coincident, &mut new_ast)
1266                .await
1267                .map_err(KclErrorWithOutputs::no_outputs)?,
1268            Constraint::Distance(distance) => self
1269                .add_distance(sketch, distance, &mut new_ast)
1270                .await
1271                .map_err(KclErrorWithOutputs::no_outputs)?,
1272            Constraint::EqualRadius(equal_radius) => self
1273                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1274                .await
1275                .map_err(KclErrorWithOutputs::no_outputs)?,
1276            Constraint::Fixed(fixed) => self
1277                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1278                .await
1279                .map_err(KclErrorWithOutputs::no_outputs)?,
1280            Constraint::HorizontalDistance(distance) => self
1281                .add_horizontal_distance(sketch, distance, &mut new_ast)
1282                .await
1283                .map_err(KclErrorWithOutputs::no_outputs)?,
1284            Constraint::VerticalDistance(distance) => self
1285                .add_vertical_distance(sketch, distance, &mut new_ast)
1286                .await
1287                .map_err(KclErrorWithOutputs::no_outputs)?,
1288            Constraint::Horizontal(horizontal) => self
1289                .add_horizontal(sketch, horizontal, &mut new_ast)
1290                .await
1291                .map_err(KclErrorWithOutputs::no_outputs)?,
1292            Constraint::LinesEqualLength(lines_equal_length) => self
1293                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1294                .await
1295                .map_err(KclErrorWithOutputs::no_outputs)?,
1296            Constraint::Midpoint(midpoint) => self
1297                .add_midpoint(sketch, midpoint, &mut new_ast)
1298                .await
1299                .map_err(KclErrorWithOutputs::no_outputs)?,
1300            Constraint::Parallel(parallel) => self
1301                .add_parallel(sketch, parallel, &mut new_ast)
1302                .await
1303                .map_err(KclErrorWithOutputs::no_outputs)?,
1304            Constraint::Perpendicular(perpendicular) => self
1305                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1306                .await
1307                .map_err(KclErrorWithOutputs::no_outputs)?,
1308            Constraint::Radius(radius) => self
1309                .add_radius(sketch, radius, &mut new_ast)
1310                .await
1311                .map_err(KclErrorWithOutputs::no_outputs)?,
1312            Constraint::Diameter(diameter) => self
1313                .add_diameter(sketch, diameter, &mut new_ast)
1314                .await
1315                .map_err(KclErrorWithOutputs::no_outputs)?,
1316            Constraint::Symmetric(symmetric) => self
1317                .add_symmetric(sketch, symmetric, &mut new_ast)
1318                .await
1319                .map_err(KclErrorWithOutputs::no_outputs)?,
1320            Constraint::Vertical(vertical) => self
1321                .add_vertical(sketch, vertical, &mut new_ast)
1322                .await
1323                .map_err(KclErrorWithOutputs::no_outputs)?,
1324            Constraint::Angle(lines_at_angle) => self
1325                .add_angle(sketch, lines_at_angle, &mut new_ast)
1326                .await
1327                .map_err(KclErrorWithOutputs::no_outputs)?,
1328            Constraint::Tangent(tangent) => self
1329                .add_tangent(sketch, tangent, &mut new_ast)
1330                .await
1331                .map_err(KclErrorWithOutputs::no_outputs)?,
1332        };
1333
1334        let result = self
1335            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1336            .await;
1337
1338        // If execution failed, restore the original state to prevent corruption
1339        if result.is_err() {
1340            self.program = original_program;
1341            self.scene_graph = original_scene_graph;
1342        }
1343
1344        result
1345    }
1346
1347    async fn chain_segment(
1348        &mut self,
1349        ctx: &ExecutorContext,
1350        version: Version,
1351        sketch: ObjectId,
1352        previous_segment_end_point_id: ObjectId,
1353        segment: SegmentCtor,
1354        _label: Option<String>,
1355    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1356        // TODO: Check version.
1357
1358        // First, add the segment (line) to get its start point ID
1359        let SegmentCtor::Line(line_ctor) = segment else {
1360            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1361                "chain_segment currently only supports Line segments, got {}",
1362                segment.human_friendly_kind_with_article(),
1363            ))));
1364        };
1365
1366        // Add the line segment first - this updates self.program and self.scene_graph
1367        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1368
1369        // Find the new line's start point ID from the updated scene graph
1370        // add_line updates self.scene_graph, so we can use that
1371        let new_line_id = first_scene_delta
1372            .new_objects
1373            .iter()
1374            .find(|&obj_id| {
1375                let obj = self.scene_graph.objects.get(obj_id.0);
1376                if let Some(obj) = obj {
1377                    matches!(
1378                        &obj.kind,
1379                        ObjectKind::Segment {
1380                            segment: Segment::Line(_)
1381                        }
1382                    )
1383                } else {
1384                    false
1385                }
1386            })
1387            .ok_or_else(|| {
1388                KclErrorWithOutputs::no_outputs(KclError::refactor(
1389                    "Failed to find new line segment in scene graph".to_string(),
1390                ))
1391            })?;
1392
1393        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1394            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1395                "New line object not found: {new_line_id:?}"
1396            )))
1397        })?;
1398
1399        let ObjectKind::Segment {
1400            segment: new_line_segment,
1401        } = &new_line_obj.kind
1402        else {
1403            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1404                "Object is not a segment: {new_line_obj:?}"
1405            ))));
1406        };
1407
1408        let Segment::Line(new_line) = new_line_segment else {
1409            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1410                "Segment is not a line: {new_line_segment:?}"
1411            ))));
1412        };
1413
1414        let new_line_start_point_id = new_line.start;
1415
1416        // Now add the coincident constraint between the previous end point and the new line's start point.
1417        let coincident = Coincident {
1418            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1419        };
1420
1421        let (final_src_delta, final_scene_delta) = self
1422            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1423            .await?;
1424
1425        // Combine new objects from the line addition and the constraint addition.
1426        // Both add_line and add_constraint now populate new_objects correctly.
1427        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1428        combined_new_objects.extend(final_scene_delta.new_objects);
1429
1430        let scene_graph_delta = SceneGraphDelta {
1431            new_graph: self.scene_graph_for_ui(),
1432            invalidates_ids: false,
1433            new_objects: combined_new_objects,
1434            exec_outcome: final_scene_delta.exec_outcome,
1435        };
1436
1437        Ok((final_src_delta, scene_graph_delta))
1438    }
1439
1440    async fn edit_constraint(
1441        &mut self,
1442        ctx: &ExecutorContext,
1443        _version: Version,
1444        sketch: ObjectId,
1445        constraint_id: ObjectId,
1446        value_expression: String,
1447    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1448        // TODO: Check version.
1449        let sketch_block_ref =
1450            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1451
1452        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1453            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1454        })?;
1455        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1456            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1457                "Object is not a constraint: {constraint_id:?}"
1458            ))));
1459        }
1460
1461        let mut new_ast = self.program.ast.clone();
1462
1463        // Parse the expression string into an AST node.
1464        let (parsed, errors) = Program::parse(&value_expression)
1465            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1466        if !errors.is_empty() {
1467            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1468                "Error parsing value expression: {errors:?}"
1469            ))));
1470        }
1471        let mut parsed = parsed.ok_or_else(|| {
1472            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1473        })?;
1474        if parsed.ast.body.is_empty() {
1475            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1476                "Empty value expression".to_string(),
1477            )));
1478        }
1479        let first = parsed.ast.body.remove(0);
1480        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1481            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1482                "Value expression must be a simple expression".to_string(),
1483            )));
1484        };
1485
1486        let new_value: ast::BinaryPart = expr_stmt
1487            .inner
1488            .expression
1489            .try_into()
1490            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1491
1492        self.mutate_ast(
1493            &mut new_ast,
1494            constraint_id,
1495            AstMutateCommand::EditConstraintValue { value: new_value },
1496        )
1497        .map_err(KclErrorWithOutputs::no_outputs)?;
1498
1499        self.execute_after_edit(
1500            ctx,
1501            sketch,
1502            sketch_block_ref,
1503            &mut new_ast,
1504            ExecuteAfterEditOptions {
1505                segment_ids_edited: Default::default(),
1506                edit_kind: EditDeleteKind::Edit,
1507                commit_solved_initial_guesses: true,
1508            },
1509        )
1510        .await
1511    }
1512
1513    async fn edit_distance_constraint_label_position(
1514        &mut self,
1515        ctx: &ExecutorContext,
1516        _version: Version,
1517        sketch: ObjectId,
1518        constraint_id: ObjectId,
1519        label_position: Point2d<Number>,
1520        anchor_segment_ids: Vec<ObjectId>,
1521    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1522        // TODO: Check version.
1523        let sketch_block_ref =
1524            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1525
1526        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1527            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1528        })?;
1529        if !matches!(
1530            &object.kind,
1531            ObjectKind::Constraint {
1532                constraint: Constraint::Distance(_)
1533                    | Constraint::HorizontalDistance(_)
1534                    | Constraint::VerticalDistance(_)
1535                    | Constraint::Radius(_)
1536                    | Constraint::Diameter(_),
1537            }
1538        ) {
1539            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1540                "Object does not support labelPosition: {constraint_id:?}"
1541            ))));
1542        }
1543
1544        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1545            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1546                "Could not convert label position to AST: {err}"
1547            )))
1548        })?;
1549        let mut new_ast = self.program.ast.clone();
1550        self.mutate_ast(
1551            &mut new_ast,
1552            constraint_id,
1553            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1554        )
1555        .map_err(KclErrorWithOutputs::no_outputs)?;
1556        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1557
1558        self.execute_after_edit(
1559            ctx,
1560            sketch,
1561            sketch_block_ref,
1562            &mut new_ast,
1563            ExecuteAfterEditOptions {
1564                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1565                edit_kind: EditDeleteKind::Edit,
1566                commit_solved_initial_guesses,
1567            },
1568        )
1569        .await
1570    }
1571
1572    /// Splitting a segment means creating a new segment, editing the old one, and then
1573    /// migrating a bunch of the constraints from the original segment to the new one
1574    /// (i.e. deleting them and re-adding them on the other segment).
1575    ///
1576    /// To keep this efficient we require as few executions as possible: we create the
1577    /// new segment first (to get its id), then do all edits and new constraints, and
1578    /// do all deletes at the end (since deletes invalidate ids).
1579    async fn batch_split_segment_operations(
1580        &mut self,
1581        ctx: &ExecutorContext,
1582        _version: Version,
1583        sketch: ObjectId,
1584        edit_segments: Vec<ExistingSegmentCtor>,
1585        add_constraints: Vec<Constraint>,
1586        delete_constraint_ids: Vec<ObjectId>,
1587        _new_segment_info: sketch::NewSegmentInfo,
1588    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1589        // TODO: Check version.
1590        let sketch_block_ref =
1591            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1592
1593        let mut new_ast = self.program.ast.clone();
1594        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1595
1596        // Step 1: Edit segments
1597        for segment in edit_segments {
1598            segment_ids_edited.insert(segment.id);
1599            match segment.ctor {
1600                SegmentCtor::Point(ctor) => self
1601                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1602                    .map_err(KclErrorWithOutputs::no_outputs)?,
1603                SegmentCtor::Line(ctor) => self
1604                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1605                    .map_err(KclErrorWithOutputs::no_outputs)?,
1606                SegmentCtor::Arc(ctor) => self
1607                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1608                    .map_err(KclErrorWithOutputs::no_outputs)?,
1609                SegmentCtor::Circle(ctor) => self
1610                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1611                    .map_err(KclErrorWithOutputs::no_outputs)?,
1612                SegmentCtor::ControlPointSpline(ctor) => self
1613                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1614                    .map_err(KclErrorWithOutputs::no_outputs)?,
1615            }
1616        }
1617
1618        // Step 2: Add all constraints
1619        for constraint in add_constraints {
1620            match constraint {
1621                Constraint::Coincident(coincident) => {
1622                    self.add_coincident(sketch, coincident, &mut new_ast)
1623                        .await
1624                        .map_err(KclErrorWithOutputs::no_outputs)?;
1625                }
1626                Constraint::Distance(distance) => {
1627                    self.add_distance(sketch, distance, &mut new_ast)
1628                        .await
1629                        .map_err(KclErrorWithOutputs::no_outputs)?;
1630                }
1631                Constraint::EqualRadius(equal_radius) => {
1632                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1633                        .await
1634                        .map_err(KclErrorWithOutputs::no_outputs)?;
1635                }
1636                Constraint::Fixed(fixed) => {
1637                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1638                        .await
1639                        .map_err(KclErrorWithOutputs::no_outputs)?;
1640                }
1641                Constraint::HorizontalDistance(distance) => {
1642                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1643                        .await
1644                        .map_err(KclErrorWithOutputs::no_outputs)?;
1645                }
1646                Constraint::VerticalDistance(distance) => {
1647                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1648                        .await
1649                        .map_err(KclErrorWithOutputs::no_outputs)?;
1650                }
1651                Constraint::Horizontal(horizontal) => {
1652                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1653                        .await
1654                        .map_err(KclErrorWithOutputs::no_outputs)?;
1655                }
1656                Constraint::LinesEqualLength(lines_equal_length) => {
1657                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1658                        .await
1659                        .map_err(KclErrorWithOutputs::no_outputs)?;
1660                }
1661                Constraint::Midpoint(midpoint) => {
1662                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1663                        .await
1664                        .map_err(KclErrorWithOutputs::no_outputs)?;
1665                }
1666                Constraint::Parallel(parallel) => {
1667                    self.add_parallel(sketch, parallel, &mut new_ast)
1668                        .await
1669                        .map_err(KclErrorWithOutputs::no_outputs)?;
1670                }
1671                Constraint::Perpendicular(perpendicular) => {
1672                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1673                        .await
1674                        .map_err(KclErrorWithOutputs::no_outputs)?;
1675                }
1676                Constraint::Vertical(vertical) => {
1677                    self.add_vertical(sketch, vertical, &mut new_ast)
1678                        .await
1679                        .map_err(KclErrorWithOutputs::no_outputs)?;
1680                }
1681                Constraint::Diameter(diameter) => {
1682                    self.add_diameter(sketch, diameter, &mut new_ast)
1683                        .await
1684                        .map_err(KclErrorWithOutputs::no_outputs)?;
1685                }
1686                Constraint::Radius(radius) => {
1687                    self.add_radius(sketch, radius, &mut new_ast)
1688                        .await
1689                        .map_err(KclErrorWithOutputs::no_outputs)?;
1690                }
1691                Constraint::Symmetric(symmetric) => {
1692                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1693                        .await
1694                        .map_err(KclErrorWithOutputs::no_outputs)?;
1695                }
1696                Constraint::Angle(angle) => {
1697                    self.add_angle(sketch, angle, &mut new_ast)
1698                        .await
1699                        .map_err(KclErrorWithOutputs::no_outputs)?;
1700                }
1701                Constraint::Tangent(tangent) => {
1702                    self.add_tangent(sketch, tangent, &mut new_ast)
1703                        .await
1704                        .map_err(KclErrorWithOutputs::no_outputs)?;
1705                }
1706            }
1707        }
1708
1709        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1710        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1711
1712        let has_constraint_deletions = !constraint_ids_set.is_empty();
1713        for constraint_id in constraint_ids_set {
1714            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1715                .map_err(KclErrorWithOutputs::no_outputs)?;
1716        }
1717
1718        // Step 4: Execute once at the end
1719        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1720        // But we'll manually set invalidates_ids: true if we deleted constraints
1721        let (source_delta, mut scene_graph_delta) = self
1722            .execute_after_edit(
1723                ctx,
1724                sketch,
1725                sketch_block_ref,
1726                &mut new_ast,
1727                ExecuteAfterEditOptions {
1728                    segment_ids_edited,
1729                    edit_kind: EditDeleteKind::Edit,
1730                    commit_solved_initial_guesses: true,
1731                },
1732            )
1733            .await?;
1734
1735        // If we deleted constraints, set invalidates_ids: true
1736        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1737        if has_constraint_deletions {
1738            scene_graph_delta.invalidates_ids = true;
1739        }
1740
1741        Ok((source_delta, scene_graph_delta))
1742    }
1743
1744    async fn batch_tail_cut_operations(
1745        &mut self,
1746        ctx: &ExecutorContext,
1747        _version: Version,
1748        sketch: ObjectId,
1749        edit_segments: Vec<ExistingSegmentCtor>,
1750        add_constraints: Vec<Constraint>,
1751        delete_constraint_ids: Vec<ObjectId>,
1752        additional_edited_segment_ids: Vec<ObjectId>,
1753    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1754        let sketch_block_ref =
1755            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1756
1757        let mut new_ast = self.program.ast.clone();
1758        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1759
1760        // Step 1: Edit segments (usually a single segment for tail cut)
1761        for segment in edit_segments {
1762            segment_ids_edited.insert(segment.id);
1763            match segment.ctor {
1764                SegmentCtor::Point(ctor) => self
1765                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1766                    .map_err(KclErrorWithOutputs::no_outputs)?,
1767                SegmentCtor::Line(ctor) => self
1768                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1769                    .map_err(KclErrorWithOutputs::no_outputs)?,
1770                SegmentCtor::Arc(ctor) => self
1771                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1772                    .map_err(KclErrorWithOutputs::no_outputs)?,
1773                SegmentCtor::Circle(ctor) => self
1774                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1775                    .map_err(KclErrorWithOutputs::no_outputs)?,
1776                SegmentCtor::ControlPointSpline(ctor) => self
1777                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1778                    .map_err(KclErrorWithOutputs::no_outputs)?,
1779            }
1780        }
1781
1782        segment_ids_edited.extend(additional_edited_segment_ids);
1783
1784        // Step 2: Add coincident constraints
1785        for constraint in add_constraints {
1786            match constraint {
1787                Constraint::Coincident(coincident) => {
1788                    self.add_coincident(sketch, coincident, &mut new_ast)
1789                        .await
1790                        .map_err(KclErrorWithOutputs::no_outputs)?;
1791                }
1792                other => {
1793                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1794                        "unsupported constraint in tail cut batch: {other:?}"
1795                    ))));
1796                }
1797            }
1798        }
1799
1800        // Step 3: Delete constraints (if any)
1801        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1802
1803        let has_constraint_deletions = !constraint_ids_set.is_empty();
1804        for constraint_id in constraint_ids_set {
1805            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1806                .map_err(KclErrorWithOutputs::no_outputs)?;
1807        }
1808
1809        // Step 4: Single execute_after_edit
1810        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1811        // But we'll manually set invalidates_ids: true if we deleted constraints
1812        let (source_delta, mut scene_graph_delta) = self
1813            .execute_after_edit(
1814                ctx,
1815                sketch,
1816                sketch_block_ref,
1817                &mut new_ast,
1818                ExecuteAfterEditOptions {
1819                    segment_ids_edited,
1820                    edit_kind: EditDeleteKind::Edit,
1821                    commit_solved_initial_guesses: true,
1822                },
1823            )
1824            .await?;
1825
1826        // If we deleted constraints, set invalidates_ids: true
1827        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1828        if has_constraint_deletions {
1829            scene_graph_delta.invalidates_ids = true;
1830        }
1831
1832        Ok((source_delta, scene_graph_delta))
1833    }
1834}
1835
1836impl FrontendState {
1837    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1838        self.program = program.clone();
1839
1840        // Execute so that the objects are updated and available for the next
1841        // API call.
1842        // This always uses engine execution (not mock) so that things are cached.
1843        // Engine execution now runs freedom analysis automatically.
1844        // Keep existing checkpoints alive here. History may still reference
1845        // older committed sketch states across a direct-edit boundary, and a
1846        // checkpoint restore is a full state replacement anyway. We append a
1847        // fresh baseline checkpoint after the full execution below.
1848        // Clear the freedom cache since IDs might have changed after direct editing
1849        // and we're about to run freedom analysis which will repopulate it.
1850        self.point_freedom_cache.clear();
1851        match ctx.run_with_caching(program).await {
1852            Ok(outcome) => {
1853                let outcome = self.update_state_after_exec(outcome, true);
1854                let checkpoint_id = self
1855                    .create_sketch_checkpoint(outcome.clone())
1856                    .await
1857                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1858                Ok(SetProgramOutcome::Success {
1859                    scene_graph: Box::new(self.scene_graph_for_ui()),
1860                    exec_outcome: Box::new(outcome),
1861                    checkpoint_id: Some(checkpoint_id),
1862                })
1863            }
1864            Err(mut err) => {
1865                // Don't return an error just because execution failed. Instead,
1866                // update state as much as possible.
1867                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1868                self.update_state_after_exec(outcome, true);
1869                err.scene_graph = Some(self.scene_graph_for_ui());
1870                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1871            }
1872        }
1873    }
1874
1875    /// Decorate engine execution such that our state is updated and the scene
1876    /// graph is added to the return.
1877    pub async fn engine_execute(
1878        &mut self,
1879        ctx: &ExecutorContext,
1880        program: Program,
1881    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1882        self.program = program.clone();
1883
1884        // Engine execution now runs freedom analysis automatically. Clear the
1885        // freedom cache since IDs might have changed after direct editing, and
1886        // we're about to run freedom analysis which will repopulate it.
1887        self.point_freedom_cache.clear();
1888        match ctx.run_with_caching(program).await {
1889            Ok(outcome) => {
1890                let outcome = self.update_state_after_exec(outcome, true);
1891                Ok(SceneGraphDelta {
1892                    new_graph: self.scene_graph_for_ui(),
1893                    exec_outcome: outcome,
1894                    // We don't know what the new objects are.
1895                    new_objects: Default::default(),
1896                    // We don't know if IDs were invalidated.
1897                    invalidates_ids: Default::default(),
1898                })
1899            }
1900            Err(mut err) => {
1901                // Update state as much as possible, even when there's an error.
1902                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1903                self.update_state_after_exec(outcome, true);
1904                err.scene_graph = Some(self.scene_graph_for_ui());
1905                Err(err)
1906            }
1907        }
1908    }
1909
1910    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1911        if matches!(err.error, KclError::EngineHangup { .. }) {
1912            // It's not ideal to special-case this, but this error is very
1913            // common during development, and it causes confusing downstream
1914            // errors that have nothing to do with the actual problem.
1915            return Err(err);
1916        }
1917
1918        let KclErrorWithOutputs {
1919            error,
1920            mut non_fatal,
1921            variables,
1922            operations,
1923            artifact_graph,
1924            scene_objects,
1925            source_range_to_object,
1926            var_solutions,
1927            filenames,
1928            default_planes,
1929            ..
1930        } = err;
1931
1932        if let Some(source_range) = error.source_ranges().first() {
1933            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1934        } else {
1935            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1936        }
1937
1938        Ok(ExecOutcome {
1939            variables,
1940            filenames,
1941            operations,
1942            artifact_graph,
1943            scene_objects,
1944            source_range_to_object,
1945            var_solutions,
1946            issues: non_fatal,
1947            default_planes,
1948        })
1949    }
1950
1951    async fn add_point(
1952        &mut self,
1953        ctx: &ExecutorContext,
1954        sketch: ObjectId,
1955        ctor: PointCtor,
1956    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1957        // Create updated KCL source from args.
1958        let at_ast = to_ast_point2d(&ctor.position)
1959            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1960        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1961            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1962            unlabeled: None,
1963            arguments: vec![ast::LabeledArg {
1964                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1965                arg: at_ast,
1966            }],
1967            digest: None,
1968            non_code_meta: Default::default(),
1969        })));
1970
1971        // Look up existing sketch.
1972        let sketch_id = sketch;
1973        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1974            #[cfg(target_arch = "wasm32")]
1975            web_sys::console::error_1(
1976                &format!(
1977                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1978                    &self.scene_graph.objects
1979                )
1980                .into(),
1981            );
1982            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1983        })?;
1984        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1985            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1986                "Object is not a sketch, it is {}",
1987                sketch_object.kind.human_friendly_kind_with_article(),
1988            ))));
1989        };
1990        // Add the point to the AST of the sketch block.
1991        let mut new_ast = self.program.ast.clone();
1992        let (sketch_block_ref, _) = self
1993            .mutate_ast(
1994                &mut new_ast,
1995                sketch_id,
1996                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1997            )
1998            .map_err(KclErrorWithOutputs::no_outputs)?;
1999        // Convert to string source to create real source ranges.
2000        let new_source = source_from_ast(&new_ast);
2001        // Parse the new KCL source.
2002        let (new_program, errors) = Program::parse(&new_source)
2003            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2004        if !errors.is_empty() {
2005            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2006                "Error parsing KCL source after adding point: {errors:?}"
2007            ))));
2008        }
2009        let Some(new_program) = new_program else {
2010            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2011                "No AST produced after adding point".to_string(),
2012            )));
2013        };
2014
2015        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2016            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2017                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2018            )))
2019        })?;
2020
2021        // Make sure to only set this if there are no errors.
2022        self.program = new_program.clone();
2023
2024        // Truncate after the sketch block for mock execution.
2025        let mut truncated_program = new_program;
2026        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2027            .map_err(KclErrorWithOutputs::no_outputs)?;
2028
2029        // Execute.
2030        let outcome = ctx
2031            .run_mock(
2032                &truncated_program,
2033                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2034            )
2035            .await?;
2036
2037        let new_object_ids = {
2038            let make_err =
2039                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2040            let segment_id = outcome
2041                .source_range_to_object
2042                .get(&point_node_ref.range)
2043                .copied()
2044                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2045            let segment_object = outcome
2046                .scene_objects
2047                .get(segment_id.0)
2048                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2049            let ObjectKind::Segment { segment } = &segment_object.kind else {
2050                return Err(make_err(format!(
2051                    "Object is not a segment, it is {}",
2052                    segment_object.kind.human_friendly_kind_with_article()
2053                )));
2054            };
2055            let Segment::Point(_) = segment else {
2056                return Err(make_err(format!(
2057                    "Segment is not a point, it is {}",
2058                    segment.human_friendly_kind_with_article()
2059                )));
2060            };
2061            vec![segment_id]
2062        };
2063        let src_delta = SourceDelta { text: new_source };
2064        // Uses .no_freedom_analysis() so freedom_analysis: false
2065        let outcome = self.update_state_after_exec(outcome, false);
2066        let scene_graph_delta = SceneGraphDelta {
2067            new_graph: self.scene_graph_for_ui(),
2068            invalidates_ids: false,
2069            new_objects: new_object_ids,
2070            exec_outcome: outcome,
2071        };
2072        Ok((src_delta, scene_graph_delta))
2073    }
2074
2075    async fn add_line(
2076        &mut self,
2077        ctx: &ExecutorContext,
2078        sketch: ObjectId,
2079        ctor: LineCtor,
2080    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2081        // Create updated KCL source from args.
2082        let start_ast = to_ast_point2d(&ctor.start)
2083            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084        let end_ast = to_ast_point2d(&ctor.end)
2085            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2086        let mut arguments = vec![
2087            ast::LabeledArg {
2088                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2089                arg: start_ast,
2090            },
2091            ast::LabeledArg {
2092                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2093                arg: end_ast,
2094            },
2095        ];
2096        // Add construction kwarg if construction is Some(true)
2097        if ctor.construction == Some(true) {
2098            arguments.push(ast::LabeledArg {
2099                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2100                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2101                    value: ast::LiteralValue::Bool(true),
2102                    raw: "true".to_string(),
2103                    digest: None,
2104                }))),
2105            });
2106        }
2107        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2108            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2109            unlabeled: None,
2110            arguments,
2111            digest: None,
2112            non_code_meta: Default::default(),
2113        })));
2114
2115        // Look up existing sketch.
2116        let sketch_id = sketch;
2117        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2118            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2119        })?;
2120        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2121            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2122                "Object is not a sketch, it is {}",
2123                sketch_object.kind.human_friendly_kind_with_article(),
2124            ))));
2125        };
2126        // Add the line to the AST of the sketch block.
2127        let mut new_ast = self.program.ast.clone();
2128        let (sketch_block_ref, _) = self
2129            .mutate_ast(
2130                &mut new_ast,
2131                sketch_id,
2132                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2133            )
2134            .map_err(KclErrorWithOutputs::no_outputs)?;
2135        // Convert to string source to create real source ranges.
2136        let new_source = source_from_ast(&new_ast);
2137        // Parse the new KCL source.
2138        let (new_program, errors) = Program::parse(&new_source)
2139            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2140        if !errors.is_empty() {
2141            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2142                "Error parsing KCL source after adding line: {errors:?}"
2143            ))));
2144        }
2145        let Some(new_program) = new_program else {
2146            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2147                "No AST produced after adding line".to_string(),
2148            )));
2149        };
2150
2151        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2152            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2153                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2154            )))
2155        })?;
2156
2157        // Make sure to only set this if there are no errors.
2158        self.program = new_program.clone();
2159
2160        // Truncate after the sketch block for mock execution.
2161        let mut truncated_program = new_program;
2162        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2163            .map_err(KclErrorWithOutputs::no_outputs)?;
2164
2165        // Execute.
2166        let outcome = ctx
2167            .run_mock(
2168                &truncated_program,
2169                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2170            )
2171            .await?;
2172
2173        let new_object_ids = {
2174            let make_err =
2175                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2176            let segment_id = outcome
2177                .source_range_to_object
2178                .get(&line_node_ref.range)
2179                .copied()
2180                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2181            let segment_object = outcome
2182                .scene_object_by_id(segment_id)
2183                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2184            let ObjectKind::Segment { segment } = &segment_object.kind else {
2185                return Err(make_err(format!(
2186                    "Object is not a segment, it is {}",
2187                    segment_object.kind.human_friendly_kind_with_article()
2188                )));
2189            };
2190            let Segment::Line(line) = segment else {
2191                return Err(make_err(format!(
2192                    "Segment is not a line, it is {}",
2193                    segment.human_friendly_kind_with_article()
2194                )));
2195            };
2196            vec![line.start, line.end, segment_id]
2197        };
2198        let src_delta = SourceDelta { text: new_source };
2199        // Uses .no_freedom_analysis() so freedom_analysis: false
2200        let outcome = self.update_state_after_exec(outcome, false);
2201        let scene_graph_delta = SceneGraphDelta {
2202            new_graph: self.scene_graph_for_ui(),
2203            invalidates_ids: false,
2204            new_objects: new_object_ids,
2205            exec_outcome: outcome,
2206        };
2207        Ok((src_delta, scene_graph_delta))
2208    }
2209
2210    async fn add_arc(
2211        &mut self,
2212        ctx: &ExecutorContext,
2213        sketch: ObjectId,
2214        ctor: ArcCtor,
2215    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2216        // Create updated KCL source from args.
2217        let start_ast = to_ast_point2d(&ctor.start)
2218            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219        let end_ast = to_ast_point2d(&ctor.end)
2220            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221        let center_ast = to_ast_point2d(&ctor.center)
2222            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2223        let mut arguments = vec![
2224            ast::LabeledArg {
2225                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2226                arg: start_ast,
2227            },
2228            ast::LabeledArg {
2229                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2230                arg: end_ast,
2231            },
2232            ast::LabeledArg {
2233                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2234                arg: center_ast,
2235            },
2236        ];
2237        // Add construction kwarg if construction is Some(true)
2238        if ctor.construction == Some(true) {
2239            arguments.push(ast::LabeledArg {
2240                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2241                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2242                    value: ast::LiteralValue::Bool(true),
2243                    raw: "true".to_string(),
2244                    digest: None,
2245                }))),
2246            });
2247        }
2248        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2249            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2250            unlabeled: None,
2251            arguments,
2252            digest: None,
2253            non_code_meta: Default::default(),
2254        })));
2255
2256        // Look up existing sketch.
2257        let sketch_id = sketch;
2258        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2259            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2260        })?;
2261        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2262            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2263                "Object is not a sketch, it is {}",
2264                sketch_object.kind.human_friendly_kind_with_article(),
2265            ))));
2266        };
2267        // Add the arc to the AST of the sketch block.
2268        let mut new_ast = self.program.ast.clone();
2269        let (sketch_block_ref, _) = self
2270            .mutate_ast(
2271                &mut new_ast,
2272                sketch_id,
2273                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2274            )
2275            .map_err(KclErrorWithOutputs::no_outputs)?;
2276        // Convert to string source to create real source ranges.
2277        let new_source = source_from_ast(&new_ast);
2278        // Parse the new KCL source.
2279        let (new_program, errors) = Program::parse(&new_source)
2280            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2281        if !errors.is_empty() {
2282            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2283                "Error parsing KCL source after adding arc: {errors:?}"
2284            ))));
2285        }
2286        let Some(new_program) = new_program else {
2287            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2288                "No AST produced after adding arc".to_string(),
2289            )));
2290        };
2291
2292        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2293            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2294                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2295            )))
2296        })?;
2297
2298        // Make sure to only set this if there are no errors.
2299        self.program = new_program.clone();
2300
2301        // Truncate after the sketch block for mock execution.
2302        let mut truncated_program = new_program;
2303        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2304            .map_err(KclErrorWithOutputs::no_outputs)?;
2305
2306        // Execute.
2307        let outcome = ctx
2308            .run_mock(
2309                &truncated_program,
2310                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2311            )
2312            .await?;
2313
2314        let new_object_ids = {
2315            let make_err =
2316                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2317            let segment_id = outcome
2318                .source_range_to_object
2319                .get(&arc_node_ref.range)
2320                .copied()
2321                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2322            let segment_object = outcome
2323                .scene_objects
2324                .get(segment_id.0)
2325                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2326            let ObjectKind::Segment { segment } = &segment_object.kind else {
2327                return Err(make_err(format!(
2328                    "Object is not a segment, it is {}",
2329                    segment_object.kind.human_friendly_kind_with_article()
2330                )));
2331            };
2332            let Segment::Arc(arc) = segment else {
2333                return Err(make_err(format!(
2334                    "Segment is not an arc, it is {}",
2335                    segment.human_friendly_kind_with_article()
2336                )));
2337            };
2338            vec![arc.start, arc.end, arc.center, segment_id]
2339        };
2340        let src_delta = SourceDelta { text: new_source };
2341        // Uses .no_freedom_analysis() so freedom_analysis: false
2342        let outcome = self.update_state_after_exec(outcome, false);
2343        let scene_graph_delta = SceneGraphDelta {
2344            new_graph: self.scene_graph_for_ui(),
2345            invalidates_ids: false,
2346            new_objects: new_object_ids,
2347            exec_outcome: outcome,
2348        };
2349        Ok((src_delta, scene_graph_delta))
2350    }
2351
2352    async fn add_circle(
2353        &mut self,
2354        ctx: &ExecutorContext,
2355        sketch: ObjectId,
2356        ctor: CircleCtor,
2357    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2358        // Create updated KCL source from args.
2359        let start_ast = to_ast_point2d(&ctor.start)
2360            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2361        let center_ast = to_ast_point2d(&ctor.center)
2362            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2363        let mut arguments = vec![
2364            ast::LabeledArg {
2365                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2366                arg: start_ast,
2367            },
2368            ast::LabeledArg {
2369                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2370                arg: center_ast,
2371            },
2372        ];
2373        // Add construction kwarg if construction is Some(true)
2374        if ctor.construction == Some(true) {
2375            arguments.push(ast::LabeledArg {
2376                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2377                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2378                    value: ast::LiteralValue::Bool(true),
2379                    raw: "true".to_string(),
2380                    digest: None,
2381                }))),
2382            });
2383        }
2384        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2385            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2386            unlabeled: None,
2387            arguments,
2388            digest: None,
2389            non_code_meta: Default::default(),
2390        })));
2391
2392        // Look up existing sketch.
2393        let sketch_id = sketch;
2394        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2395            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2396        })?;
2397        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2398            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2399                "Object is not a sketch, it is {}",
2400                sketch_object.kind.human_friendly_kind_with_article(),
2401            ))));
2402        };
2403        // Add the circle to the AST of the sketch block.
2404        let mut new_ast = self.program.ast.clone();
2405        let (sketch_block_ref, _) = self
2406            .mutate_ast(
2407                &mut new_ast,
2408                sketch_id,
2409                AstMutateCommand::AddSketchBlockVarDecl {
2410                    prefix: CIRCLE_VARIABLE.to_owned(),
2411                    expr: circle_ast,
2412                },
2413            )
2414            .map_err(KclErrorWithOutputs::no_outputs)?;
2415        // Convert to string source to create real source ranges.
2416        let new_source = source_from_ast(&new_ast);
2417        // Parse the new KCL source.
2418        let (new_program, errors) = Program::parse(&new_source)
2419            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2420        if !errors.is_empty() {
2421            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2422                "Error parsing KCL source after adding circle: {errors:?}"
2423            ))));
2424        }
2425        let Some(new_program) = new_program else {
2426            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2427                "No AST produced after adding circle".to_string(),
2428            )));
2429        };
2430
2431        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2432            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2433                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2434            )))
2435        })?;
2436
2437        // Make sure to only set this if there are no errors.
2438        self.program = new_program.clone();
2439
2440        // Truncate after the sketch block for mock execution.
2441        let mut truncated_program = new_program;
2442        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2443            .map_err(KclErrorWithOutputs::no_outputs)?;
2444
2445        // Execute.
2446        let outcome = ctx
2447            .run_mock(
2448                &truncated_program,
2449                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2450            )
2451            .await?;
2452
2453        let new_object_ids = {
2454            let make_err =
2455                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2456            let segment_id = outcome
2457                .source_range_to_object
2458                .get(&circle_node_ref.range)
2459                .copied()
2460                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2461            let segment_object = outcome
2462                .scene_objects
2463                .get(segment_id.0)
2464                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2465            let ObjectKind::Segment { segment } = &segment_object.kind else {
2466                return Err(make_err(format!(
2467                    "Object is not a segment, it is {}",
2468                    segment_object.kind.human_friendly_kind_with_article()
2469                )));
2470            };
2471            let Segment::Circle(circle) = segment else {
2472                return Err(make_err(format!(
2473                    "Segment is not a circle, it is {}",
2474                    segment.human_friendly_kind_with_article()
2475                )));
2476            };
2477            vec![circle.start, circle.center, segment_id]
2478        };
2479        let src_delta = SourceDelta { text: new_source };
2480        // Uses .no_freedom_analysis() so freedom_analysis: false
2481        let outcome = self.update_state_after_exec(outcome, false);
2482        let scene_graph_delta = SceneGraphDelta {
2483            new_graph: self.scene_graph_for_ui(),
2484            invalidates_ids: false,
2485            new_objects: new_object_ids,
2486            exec_outcome: outcome,
2487        };
2488        Ok((src_delta, scene_graph_delta))
2489    }
2490
2491    async fn add_control_point_spline(
2492        &mut self,
2493        ctx: &ExecutorContext,
2494        sketch: ObjectId,
2495        ctor: ControlPointSplineCtor,
2496    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2497        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2498            .map_err(KclErrorWithOutputs::no_outputs)?;
2499
2500        let points_ast = to_ast_point2d_array(&ctor.points)
2501            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2502        let mut arguments = vec![ast::LabeledArg {
2503            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2504            arg: points_ast,
2505        }];
2506        if ctor.construction == Some(true) {
2507            arguments.push(ast::LabeledArg {
2508                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2509                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2510                    value: ast::LiteralValue::Bool(true),
2511                    raw: "true".to_string(),
2512                    digest: None,
2513                }))),
2514            });
2515        }
2516        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2517            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2518            unlabeled: None,
2519            arguments,
2520            digest: None,
2521            non_code_meta: Default::default(),
2522        })));
2523
2524        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2525            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2526        })?;
2527        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2528            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2529                "Object is not a sketch, it is {}",
2530                sketch_object.kind.human_friendly_kind_with_article(),
2531            ))));
2532        };
2533
2534        let mut new_ast = new_program.ast.clone();
2535        let (sketch_block_ref, _) = self
2536            .mutate_ast(
2537                &mut new_ast,
2538                sketch,
2539                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2540            )
2541            .map_err(KclErrorWithOutputs::no_outputs)?;
2542        let new_source = source_from_ast(&new_ast);
2543        let (new_program, errors) = Program::parse(&new_source)
2544            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2545        if !errors.is_empty() {
2546            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2547                "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2548            ))));
2549        }
2550        let Some(new_program) = new_program else {
2551            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2552                "No AST produced after adding controlPointSpline".to_string(),
2553            )));
2554        };
2555
2556        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2557            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2558                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2559            )))
2560        })?;
2561
2562        self.program = new_program.clone();
2563
2564        let mut truncated_program = new_program;
2565        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2566            .map_err(KclErrorWithOutputs::no_outputs)?;
2567
2568        let outcome = ctx
2569            .run_mock(
2570                &truncated_program,
2571                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2572            )
2573            .await?;
2574
2575        let new_object_ids = {
2576            let make_err =
2577                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2578            let segment_id = outcome
2579                .source_range_to_object
2580                .get(&spline_node_ref.range)
2581                .copied()
2582                .ok_or_else(|| {
2583                    make_err(format!(
2584                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2585                    ))
2586                })?;
2587            let segment_object = outcome
2588                .scene_objects
2589                .get(segment_id.0)
2590                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2591            let ObjectKind::Segment { segment } = &segment_object.kind else {
2592                return Err(make_err(format!(
2593                    "Object is not a segment, it is {}",
2594                    segment_object.kind.human_friendly_kind_with_article()
2595                )));
2596            };
2597            let Segment::ControlPointSpline(spline) = segment else {
2598                return Err(make_err(format!(
2599                    "Segment is not a control point spline, it is {}",
2600                    segment.human_friendly_kind_with_article()
2601                )));
2602            };
2603
2604            let mut ids = outcome
2605                .scene_objects
2606                .iter()
2607                .filter_map(|obj| match &obj.kind {
2608                    ObjectKind::Segment {
2609                        segment: Segment::Line(line),
2610                    } if line.owner == Some(segment_id) => Some(obj.id),
2611                    _ => None,
2612                })
2613                .collect::<Vec<_>>();
2614            ids.extend(spline.controls.clone());
2615            ids.push(segment_id);
2616            ids
2617        };
2618        let src_delta = SourceDelta { text: new_source };
2619        let outcome = self.update_state_after_exec(outcome, false);
2620        let scene_graph_delta = SceneGraphDelta {
2621            new_graph: self.scene_graph_for_ui(),
2622            invalidates_ids: false,
2623            new_objects: new_object_ids,
2624            exec_outcome: outcome,
2625        };
2626        Ok((src_delta, scene_graph_delta))
2627    }
2628
2629    fn edit_point(
2630        &mut self,
2631        new_ast: &mut ast::Node<ast::Program>,
2632        sketch: ObjectId,
2633        point: ObjectId,
2634        ctor: PointCtor,
2635    ) -> Result<(), KclError> {
2636        // Create updated KCL source from args.
2637        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2638
2639        // Look up existing sketch.
2640        let sketch_id = sketch;
2641        let sketch_object = self
2642            .scene_graph
2643            .objects
2644            .get(sketch_id.0)
2645            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2646        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2647            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2648        };
2649        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2650            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2651        })?;
2652        // Look up existing point.
2653        let point_id = point;
2654        let point_object = self
2655            .scene_graph
2656            .objects
2657            .get(point_id.0)
2658            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2659        let ObjectKind::Segment {
2660            segment: Segment::Point(point),
2661        } = &point_object.kind
2662        else {
2663            return Err(KclError::refactor(format!(
2664                "Object is not a point segment: {point_object:?}"
2665            )));
2666        };
2667
2668        // If the point is part of a line or arc, edit the line/arc instead.
2669        if let Some(owner_id) = point.owner {
2670            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2671                KclError::refactor(format!(
2672                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2673                ))
2674            })?;
2675            let ObjectKind::Segment { segment } = &owner_object.kind else {
2676                return Err(KclError::refactor(format!(
2677                    "Internal: Owner of point is not a segment, but found {}",
2678                    owner_object.kind.human_friendly_kind_with_article()
2679                )));
2680            };
2681
2682            // Handle Line owner
2683            if let Segment::Line(line) = segment {
2684                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2685                    return Err(KclError::refactor(format!(
2686                        "Internal: Owner of point does not have line ctor, but found {}",
2687                        line.ctor.human_friendly_kind_with_article()
2688                    )));
2689                };
2690                let mut line_ctor = line_ctor.clone();
2691                // Which end of the line is this point?
2692                if line.start == point_id {
2693                    line_ctor.start = ctor.position;
2694                } else if line.end == point_id {
2695                    line_ctor.end = ctor.position;
2696                } else {
2697                    return Err(KclError::refactor(format!(
2698                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2699                    )));
2700                }
2701                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2702            }
2703
2704            // Handle Arc owner
2705            if let Segment::Arc(arc) = segment {
2706                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2707                    return Err(KclError::refactor(format!(
2708                        "Internal: Owner of point does not have arc ctor, but found {}",
2709                        arc.ctor.human_friendly_kind_with_article()
2710                    )));
2711                };
2712                let mut arc_ctor = arc_ctor.clone();
2713                // Which point of the arc is this? (center, start, or end)
2714                if arc.center == point_id {
2715                    arc_ctor.center = ctor.position;
2716                } else if arc.start == point_id {
2717                    arc_ctor.start = ctor.position;
2718                } else if arc.end == point_id {
2719                    arc_ctor.end = ctor.position;
2720                } else {
2721                    return Err(KclError::refactor(format!(
2722                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2723                    )));
2724                }
2725                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2726            }
2727
2728            // Handle Circle owner
2729            if let Segment::Circle(circle) = segment {
2730                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2731                    return Err(KclError::refactor(format!(
2732                        "Internal: Owner of point does not have circle ctor, but found {}",
2733                        circle.ctor.human_friendly_kind_with_article()
2734                    )));
2735                };
2736                let mut circle_ctor = circle_ctor.clone();
2737                if circle.center == point_id {
2738                    circle_ctor.center = ctor.position;
2739                } else if circle.start == point_id {
2740                    circle_ctor.start = ctor.position;
2741                } else {
2742                    return Err(KclError::refactor(format!(
2743                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2744                    )));
2745                }
2746                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2747            }
2748
2749            if let Segment::ControlPointSpline(spline) = segment {
2750                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2751                    return Err(KclError::refactor(format!(
2752                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2753                        spline.ctor.human_friendly_kind_with_article()
2754                    )));
2755                };
2756                let mut spline_ctor = spline_ctor.clone();
2757                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2758                    return Err(KclError::refactor(format!(
2759                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2760                    )));
2761                };
2762                spline_ctor.points[control_index] = ctor.position;
2763                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2764            }
2765
2766            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2767            // (fall through to the point editing logic below)
2768        }
2769
2770        // Modify the point AST.
2771        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2772        Ok(())
2773    }
2774
2775    fn edit_line(
2776        &mut self,
2777        new_ast: &mut ast::Node<ast::Program>,
2778        sketch: ObjectId,
2779        line: ObjectId,
2780        ctor: LineCtor,
2781    ) -> Result<(), KclError> {
2782        // Create updated KCL source from args.
2783        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2784        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2785
2786        // Look up existing sketch.
2787        let sketch_id = sketch;
2788        let sketch_object = self
2789            .scene_graph
2790            .objects
2791            .get(sketch_id.0)
2792            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2793        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2794            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2795        };
2796        sketch
2797            .segments
2798            .iter()
2799            .find(|o| **o == line)
2800            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2801        // Look up existing line.
2802        let line_id = line;
2803        let line_object = self
2804            .scene_graph
2805            .objects
2806            .get(line_id.0)
2807            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2808        let ObjectKind::Segment { .. } = &line_object.kind else {
2809            let kind = line_object.kind.human_friendly_kind_with_article();
2810            return Err(KclError::refactor(format!(
2811                "This constraint only works on Segments, but you selected {kind}"
2812            )));
2813        };
2814
2815        // Modify the line AST.
2816        self.mutate_ast(
2817            new_ast,
2818            line_id,
2819            AstMutateCommand::EditLine {
2820                start: new_start_ast,
2821                end: new_end_ast,
2822                construction: ctor.construction,
2823            },
2824        )?;
2825        Ok(())
2826    }
2827
2828    fn edit_arc(
2829        &mut self,
2830        new_ast: &mut ast::Node<ast::Program>,
2831        sketch: ObjectId,
2832        arc: ObjectId,
2833        ctor: ArcCtor,
2834    ) -> Result<(), KclError> {
2835        // Create updated KCL source from args.
2836        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2837        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2838        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2839
2840        // Look up existing sketch.
2841        let sketch_id = sketch;
2842        let sketch_object = self
2843            .scene_graph
2844            .objects
2845            .get(sketch_id.0)
2846            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2847        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2848            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2849        };
2850        sketch
2851            .segments
2852            .iter()
2853            .find(|o| **o == arc)
2854            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2855        // Look up existing arc.
2856        let arc_id = arc;
2857        let arc_object = self
2858            .scene_graph
2859            .objects
2860            .get(arc_id.0)
2861            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2862        let ObjectKind::Segment { .. } = &arc_object.kind else {
2863            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2864        };
2865
2866        // Modify the arc AST.
2867        self.mutate_ast(
2868            new_ast,
2869            arc_id,
2870            AstMutateCommand::EditArc {
2871                start: new_start_ast,
2872                end: new_end_ast,
2873                center: new_center_ast,
2874                construction: ctor.construction,
2875            },
2876        )?;
2877        Ok(())
2878    }
2879
2880    fn edit_circle(
2881        &mut self,
2882        new_ast: &mut ast::Node<ast::Program>,
2883        sketch: ObjectId,
2884        circle: ObjectId,
2885        ctor: CircleCtor,
2886    ) -> Result<(), KclError> {
2887        // Create updated KCL source from args.
2888        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2889        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2890
2891        // Look up existing sketch.
2892        let sketch_id = sketch;
2893        let sketch_object = self
2894            .scene_graph
2895            .objects
2896            .get(sketch_id.0)
2897            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2898        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2899            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2900        };
2901        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2902            KclError::refactor(format!(
2903                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2904            ))
2905        })?;
2906        // Look up existing circle.
2907        let circle_id = circle;
2908        let circle_object = self
2909            .scene_graph
2910            .objects
2911            .get(circle_id.0)
2912            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2913        let ObjectKind::Segment { .. } = &circle_object.kind else {
2914            return Err(KclError::refactor(format!(
2915                "Object is not a segment: {circle_object:?}"
2916            )));
2917        };
2918
2919        // Modify the circle AST.
2920        self.mutate_ast(
2921            new_ast,
2922            circle_id,
2923            AstMutateCommand::EditCircle {
2924                start: new_start_ast,
2925                center: new_center_ast,
2926                construction: ctor.construction,
2927            },
2928        )?;
2929        Ok(())
2930    }
2931
2932    fn edit_control_point_spline(
2933        &mut self,
2934        new_ast: &mut ast::Node<ast::Program>,
2935        sketch: ObjectId,
2936        spline: ObjectId,
2937        ctor: ControlPointSplineCtor,
2938    ) -> Result<(), KclError> {
2939        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2940
2941        let sketch_object = self
2942            .scene_graph
2943            .objects
2944            .get(sketch.0)
2945            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2946        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2947            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2948        };
2949        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2950            KclError::refactor(format!(
2951                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2952            ))
2953        })?;
2954
2955        let spline_object =
2956            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2957                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2958            })?;
2959        let ObjectKind::Segment { .. } = &spline_object.kind else {
2960            return Err(KclError::refactor(format!(
2961                "Object is not a segment: {spline_object:?}"
2962            )));
2963        };
2964
2965        self.mutate_ast(
2966            new_ast,
2967            spline,
2968            AstMutateCommand::EditControlPointSpline {
2969                points: points_ast,
2970                construction: ctor.construction,
2971            },
2972        )?;
2973        Ok(())
2974    }
2975
2976    fn delete_segment(
2977        &mut self,
2978        new_ast: &mut ast::Node<ast::Program>,
2979        sketch: ObjectId,
2980        segment_id: ObjectId,
2981    ) -> Result<(), KclError> {
2982        // Look up existing sketch.
2983        let sketch_id = sketch;
2984        let sketch_object = self
2985            .scene_graph
2986            .objects
2987            .get(sketch_id.0)
2988            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2989        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2990            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2991        };
2992        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2993            KclError::refactor(format!(
2994                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2995            ))
2996        })?;
2997        // Look up existing segment.
2998        let segment_object =
2999            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3000                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3001            })?;
3002        let ObjectKind::Segment { .. } = &segment_object.kind else {
3003            return Err(KclError::refactor(format!(
3004                "Object is not a segment, it is {}",
3005                segment_object.kind.human_friendly_kind_with_article()
3006            )));
3007        };
3008
3009        // Modify the AST to remove the segment.
3010        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3011        Ok(())
3012    }
3013
3014    fn delete_constraint(
3015        &mut self,
3016        new_ast: &mut ast::Node<ast::Program>,
3017        sketch: ObjectId,
3018        constraint_id: ObjectId,
3019    ) -> Result<(), KclError> {
3020        // Look up existing sketch.
3021        let sketch_id = sketch;
3022        let sketch_object = self
3023            .scene_graph
3024            .objects
3025            .get(sketch_id.0)
3026            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3027        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3028            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3029        };
3030        sketch
3031            .constraints
3032            .iter()
3033            .find(|o| **o == constraint_id)
3034            .ok_or_else(|| {
3035                KclError::refactor(format!(
3036                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3037                ))
3038            })?;
3039        // Look up existing constraint.
3040        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3041            KclError::refactor(format!(
3042                "Constraint not found in scene graph: constraint={constraint_id:?}"
3043            ))
3044        })?;
3045        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3046            return Err(KclError::refactor(format!(
3047                "Object is not a constraint, it is {}",
3048                constraint_object.kind.human_friendly_kind_with_article()
3049            )));
3050        };
3051
3052        // Modify the AST to remove the constraint.
3053        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3054        Ok(())
3055    }
3056
3057    fn edit_coincident_constraint(
3058        &mut self,
3059        new_ast: &mut ast::Node<ast::Program>,
3060        constraint_id: ObjectId,
3061        segments: Vec<ConstraintSegment>,
3062    ) -> Result<(), KclError> {
3063        if segments.len() < 2 {
3064            return Err(KclError::refactor(format!(
3065                "Coincident constraint must have at least 2 inputs, got {}",
3066                segments.len()
3067            )));
3068        }
3069
3070        let segment_asts = segments
3071            .iter()
3072            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3073            .collect::<Result<Vec<_>, _>>()?;
3074
3075        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3076            elements: segment_asts,
3077            digest: None,
3078            non_code_meta: Default::default(),
3079        })));
3080
3081        self.mutate_ast(
3082            new_ast,
3083            constraint_id,
3084            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3085        )?;
3086        Ok(())
3087    }
3088
3089    fn edit_horizontal_points_constraint(
3090        &mut self,
3091        new_ast: &mut ast::Node<ast::Program>,
3092        constraint_id: ObjectId,
3093        points: Vec<ConstraintSegment>,
3094    ) -> Result<(), KclError> {
3095        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3096    }
3097
3098    fn edit_vertical_points_constraint(
3099        &mut self,
3100        new_ast: &mut ast::Node<ast::Program>,
3101        constraint_id: ObjectId,
3102        points: Vec<ConstraintSegment>,
3103    ) -> Result<(), KclError> {
3104        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3105    }
3106
3107    fn edit_axis_points_constraint(
3108        &mut self,
3109        new_ast: &mut ast::Node<ast::Program>,
3110        constraint_id: ObjectId,
3111        points: Vec<ConstraintSegment>,
3112        constraint_name: &str,
3113    ) -> Result<(), KclError> {
3114        if points.len() < 2 {
3115            return Err(KclError::refactor(format!(
3116                "{constraint_name} points constraint must have at least 2 points, got {}",
3117                points.len()
3118            )));
3119        }
3120
3121        let point_asts = points
3122            .iter()
3123            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3124            .collect::<Result<Vec<_>, _>>()?;
3125
3126        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3127            elements: point_asts,
3128            digest: None,
3129            non_code_meta: Default::default(),
3130        })));
3131
3132        self.mutate_ast(
3133            new_ast,
3134            constraint_id,
3135            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3136        )?;
3137        Ok(())
3138    }
3139
3140    /// updates the equalLength constraint with the given lines
3141    fn edit_equal_length_constraint(
3142        &mut self,
3143        new_ast: &mut ast::Node<ast::Program>,
3144        constraint_id: ObjectId,
3145        lines: Vec<ObjectId>,
3146    ) -> Result<(), KclError> {
3147        if lines.len() < 2 {
3148            return Err(KclError::refactor(format!(
3149                "Lines equal length constraint must have at least 2 lines, got {}",
3150                lines.len()
3151            )));
3152        }
3153
3154        let line_asts = lines
3155            .iter()
3156            .map(|line_id| {
3157                let line_object = self
3158                    .scene_graph
3159                    .objects
3160                    .get(line_id.0)
3161                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3162                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3163                    let kind = line_object.kind.human_friendly_kind_with_article();
3164                    return Err(KclError::refactor(format!(
3165                        "This constraint only works on Segments, but you selected {kind}"
3166                    )));
3167                };
3168                let Segment::Line(_) = line_segment else {
3169                    let kind = line_segment.human_friendly_kind_with_article();
3170                    return Err(KclError::refactor(format!(
3171                        "Only lines can be made equal length, but you selected {kind}"
3172                    )));
3173                };
3174
3175                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3176            })
3177            .collect::<Result<Vec<_>, _>>()?;
3178
3179        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3180            elements: line_asts,
3181            digest: None,
3182            non_code_meta: Default::default(),
3183        })));
3184
3185        self.mutate_ast(
3186            new_ast,
3187            constraint_id,
3188            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3189        )?;
3190        Ok(())
3191    }
3192
3193    /// Updates the parallel constraint with the given lines.
3194    fn edit_parallel_constraint(
3195        &mut self,
3196        new_ast: &mut ast::Node<ast::Program>,
3197        constraint_id: ObjectId,
3198        lines: Vec<ObjectId>,
3199    ) -> Result<(), KclError> {
3200        if lines.len() < 2 {
3201            return Err(KclError::refactor(format!(
3202                "Parallel constraint must have at least 2 lines, got {}",
3203                lines.len()
3204            )));
3205        }
3206
3207        let line_asts = lines
3208            .iter()
3209            .map(|line_id| {
3210                let line_object = self
3211                    .scene_graph
3212                    .objects
3213                    .get(line_id.0)
3214                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3215                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3216                    let kind = line_object.kind.human_friendly_kind_with_article();
3217                    return Err(KclError::refactor(format!(
3218                        "This constraint only works on Segments, but you selected {kind}"
3219                    )));
3220                };
3221                let Segment::Line(_) = line_segment else {
3222                    let kind = line_segment.human_friendly_kind_with_article();
3223                    return Err(KclError::refactor(format!(
3224                        "Only lines can be made parallel, but you selected {kind}"
3225                    )));
3226                };
3227
3228                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3229            })
3230            .collect::<Result<Vec<_>, _>>()?;
3231
3232        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3233            elements: line_asts,
3234            digest: None,
3235            non_code_meta: Default::default(),
3236        })));
3237
3238        self.mutate_ast(
3239            new_ast,
3240            constraint_id,
3241            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3242        )?;
3243        Ok(())
3244    }
3245
3246    /// Updates the equalRadius constraint with the given segments.
3247    fn edit_equal_radius_constraint(
3248        &mut self,
3249        new_ast: &mut ast::Node<ast::Program>,
3250        constraint_id: ObjectId,
3251        input: Vec<ObjectId>,
3252    ) -> Result<(), KclError> {
3253        if input.len() < 2 {
3254            return Err(KclError::refactor(format!(
3255                "equalRadius constraint must have at least 2 segments, got {}",
3256                input.len()
3257            )));
3258        }
3259
3260        let input_asts = input
3261            .iter()
3262            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3263            .collect::<Result<Vec<_>, _>>()?;
3264
3265        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3266            elements: input_asts,
3267            digest: None,
3268            non_code_meta: Default::default(),
3269        })));
3270
3271        self.mutate_ast(
3272            new_ast,
3273            constraint_id,
3274            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3275        )?;
3276        Ok(())
3277    }
3278
3279    async fn execute_after_edit(
3280        &mut self,
3281        ctx: &ExecutorContext,
3282        sketch: ObjectId,
3283        sketch_block_ref: AstNodeRef,
3284        new_ast: &mut ast::Node<ast::Program>,
3285        options: ExecuteAfterEditOptions,
3286    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3287        let ExecuteAfterEditOptions {
3288            segment_ids_edited,
3289            edit_kind,
3290            commit_solved_initial_guesses,
3291        } = options;
3292
3293        // Convert to string source to create real source ranges.
3294        let new_source = source_from_ast(new_ast);
3295        // Parse the new KCL source.
3296        let (new_program, errors) = Program::parse(&new_source)
3297            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3298        if !errors.is_empty() {
3299            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3300                "Error parsing KCL source after editing: {errors:?}"
3301            ))));
3302        }
3303        let Some(new_program) = new_program else {
3304            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3305                "No AST produced after editing".to_string(),
3306            )));
3307        };
3308
3309        // TODO: sketch-api: make sure to only set this if there are no errors.
3310        self.program = new_program.clone();
3311
3312        // Truncate after the sketch block for mock execution.
3313        let is_delete = edit_kind.is_delete();
3314        let truncated_program = {
3315            let mut truncated_program = new_program;
3316            only_sketch_block(
3317                &mut truncated_program.ast,
3318                &sketch_block_ref,
3319                edit_kind.to_change_kind(),
3320            )
3321            .map_err(KclErrorWithOutputs::no_outputs)?;
3322            truncated_program
3323        };
3324
3325        // Execute.
3326        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3327        let mock_config = MockConfig {
3328            sketch_block_id: Some(sketch),
3329            freedom_analysis: is_delete,
3330            segment_ids_edited: segment_ids_edited.clone(),
3331            drag_anchors,
3332            ..Default::default()
3333        };
3334        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3335
3336        // Uses freedom_analysis: is_delete
3337        let outcome = self.update_state_after_exec(outcome, is_delete);
3338
3339        let src_delta = if commit_solved_initial_guesses {
3340            self.commit_var_solutions_to_program(&outcome, "editing")?
3341        } else {
3342            SourceDelta { text: new_source }
3343        };
3344        let scene_graph_delta = SceneGraphDelta {
3345            new_graph: self.scene_graph_for_ui(),
3346            invalidates_ids: is_delete,
3347            new_objects: Vec::new(),
3348            exec_outcome: outcome,
3349        };
3350        Ok((src_delta, scene_graph_delta))
3351    }
3352
3353    async fn execute_after_delete_sketch(
3354        &mut self,
3355        ctx: &ExecutorContext,
3356        new_ast: &mut ast::Node<ast::Program>,
3357    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3358        // Convert to string source to create real source ranges.
3359        let new_source = source_from_ast(new_ast);
3360        // Parse the new KCL source.
3361        let (new_program, errors) = Program::parse(&new_source)
3362            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3363        if !errors.is_empty() {
3364            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3365                "Error parsing KCL source after editing: {errors:?}"
3366            ))));
3367        }
3368        let Some(new_program) = new_program else {
3369            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3370                "No AST produced after editing".to_string(),
3371            )));
3372        };
3373
3374        // Make sure to only set this if there are no errors.
3375        self.program = new_program.clone();
3376
3377        // We deleted the entire sketch block. It doesn't make sense to truncate
3378        // and execute only the sketch block. We execute the whole program with
3379        // a real engine.
3380
3381        // Execute.
3382        let outcome = ctx.run_with_caching(new_program).await?;
3383        let freedom_analysis_ran = true;
3384
3385        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3386
3387        let src_delta = SourceDelta { text: new_source };
3388        let scene_graph_delta = SceneGraphDelta {
3389            new_graph: self.scene_graph_for_ui(),
3390            invalidates_ids: true,
3391            new_objects: Vec::new(),
3392            exec_outcome: outcome,
3393        };
3394        Ok((src_delta, scene_graph_delta))
3395    }
3396
3397    /// Map a point object id into an AST reference expression for use in
3398    /// constraints. If the point is owned by a segment (line or arc), we
3399    /// reference the appropriate property on that segment (e.g. `line1.start`,
3400    /// `arc1.center`). Otherwise we reference the point directly.
3401    fn point_id_to_ast_reference(
3402        &self,
3403        point_id: ObjectId,
3404        new_ast: &mut ast::Node<ast::Program>,
3405    ) -> Result<ast::Expr, KclError> {
3406        let point_object = self
3407            .scene_graph
3408            .objects
3409            .get(point_id.0)
3410            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3411        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3412            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3413        };
3414        let Segment::Point(point) = point_segment else {
3415            return Err(KclError::refactor(format!(
3416                "Only points are currently supported: {point_object:?}"
3417            )));
3418        };
3419
3420        if let Some(owner_id) = point.owner {
3421            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3422                KclError::refactor(format!(
3423                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3424                ))
3425            })?;
3426            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3427                return Err(KclError::refactor(format!(
3428                    "Owner of point is not a segment, but found {}",
3429                    owner_object.kind.human_friendly_kind_with_article()
3430                )));
3431            };
3432
3433            match owner_segment {
3434                Segment::Line(line) => {
3435                    let property = if line.start == point_id {
3436                        LINE_PROPERTY_START
3437                    } else if line.end == point_id {
3438                        LINE_PROPERTY_END
3439                    } else {
3440                        return Err(KclError::refactor(format!(
3441                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3442                        )));
3443                    };
3444                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3445                }
3446                Segment::Arc(arc) => {
3447                    let property = if arc.start == point_id {
3448                        ARC_PROPERTY_START
3449                    } else if arc.end == point_id {
3450                        ARC_PROPERTY_END
3451                    } else if arc.center == point_id {
3452                        ARC_PROPERTY_CENTER
3453                    } else {
3454                        return Err(KclError::refactor(format!(
3455                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3456                        )));
3457                    };
3458                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3459                }
3460                Segment::Circle(circle) => {
3461                    let property = if circle.start == point_id {
3462                        CIRCLE_PROPERTY_START
3463                    } else if circle.center == point_id {
3464                        CIRCLE_PROPERTY_CENTER
3465                    } else {
3466                        return Err(KclError::refactor(format!(
3467                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3468                        )));
3469                    };
3470                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3471                }
3472                Segment::ControlPointSpline(spline) => {
3473                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3474                        return Err(KclError::refactor(format!(
3475                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3476                        )));
3477                    };
3478                    let owner_expr =
3479                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3480                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3481                    Ok(create_index_expression(controls_expr, index))
3482                }
3483                _ => Err(KclError::refactor(format!(
3484                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3485                ))),
3486            }
3487        } else {
3488            // Standalone point.
3489            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3490        }
3491    }
3492
3493    fn line_id_to_ast_reference(
3494        &self,
3495        line_id: ObjectId,
3496        new_ast: &mut ast::Node<ast::Program>,
3497    ) -> Result<ast::Expr, KclError> {
3498        let line_object = self
3499            .scene_graph
3500            .objects
3501            .get(line_id.0)
3502            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3503        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3504            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3505        };
3506        let Segment::Line(line) = line_segment else {
3507            return Err(KclError::refactor(format!(
3508                "Only lines are currently supported: {line_object:?}"
3509            )));
3510        };
3511
3512        if let Some(owner_id) = line.owner {
3513            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3514                KclError::refactor(format!(
3515                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3516                ))
3517            })?;
3518            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3519                return Err(KclError::refactor(format!(
3520                    "Owner of line is not a segment, but found {}",
3521                    owner_object.kind.human_friendly_kind_with_article()
3522                )));
3523            };
3524
3525            match owner_segment {
3526                Segment::ControlPointSpline(spline) => {
3527                    let Some(index) = spline
3528                        .controls
3529                        .windows(2)
3530                        .position(|window| window[0] == line.start && window[1] == line.end)
3531                    else {
3532                        return Err(KclError::refactor(format!(
3533                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3534                        )));
3535                    };
3536                    let owner_expr =
3537                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3538                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3539                    Ok(create_index_expression(edges_expr, index))
3540                }
3541                _ => Err(KclError::refactor(format!(
3542                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3543                ))),
3544            }
3545        } else {
3546            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3547        }
3548    }
3549
3550    fn coincident_segment_to_ast(
3551        &self,
3552        segment: &ConstraintSegment,
3553        new_ast: &mut ast::Node<ast::Program>,
3554    ) -> Result<ast::Expr, KclError> {
3555        match segment {
3556            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3557            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3558        }
3559    }
3560
3561    fn segment_id_to_constraint_ast_reference(
3562        &self,
3563        segment_id: ObjectId,
3564        new_ast: &mut ast::Node<ast::Program>,
3565    ) -> Result<ast::Expr, KclError> {
3566        let segment_object = self
3567            .scene_graph
3568            .objects
3569            .get(segment_id.0)
3570            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3571        let ObjectKind::Segment { segment } = &segment_object.kind else {
3572            return Err(KclError::refactor(format!(
3573                "Object is not a segment, it is {}",
3574                segment_object.kind.human_friendly_kind_with_article()
3575            )));
3576        };
3577
3578        match segment {
3579            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3580            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3581            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3582            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3583            Segment::ControlPointSpline(_) => {
3584                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3585            }
3586        }
3587    }
3588
3589    fn axis_constraint_segment_to_ast(
3590        &self,
3591        segment: &ConstraintSegment,
3592        new_ast: &mut ast::Node<ast::Program>,
3593    ) -> Result<ast::Expr, KclError> {
3594        match segment {
3595            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3596            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3597        }
3598    }
3599
3600    async fn add_coincident(
3601        &mut self,
3602        sketch: ObjectId,
3603        coincident: Coincident,
3604        new_ast: &mut ast::Node<ast::Program>,
3605    ) -> Result<AstNodeRef, KclError> {
3606        let sketch_id = sketch;
3607        for segment in &coincident.segments {
3608            let ConstraintSegment::Segment(segment_id) = segment else {
3609                continue;
3610            };
3611            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3612                continue;
3613            };
3614            if matches!(
3615                segment_object.kind,
3616                ObjectKind::Segment {
3617                    segment: Segment::ControlPointSpline(_)
3618                }
3619            ) {
3620                return Err(KclError::refactor(
3621                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3622                        .to_owned(),
3623                ));
3624            }
3625        }
3626        let segment_asts = coincident
3627            .segments
3628            .iter()
3629            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3630            .collect::<Result<Vec<_>, _>>()?;
3631        if segment_asts.len() < 2 {
3632            return Err(KclError::refactor(format!(
3633                "Coincident constraint must have at least 2 inputs, got {}",
3634                segment_asts.len()
3635            )));
3636        }
3637
3638        // Create the coincident() call using shared helper.
3639        let coincident_ast = create_coincident_ast(segment_asts);
3640
3641        // Add the line to the AST of the sketch block.
3642        let (sketch_block_ref, _) = self.mutate_ast(
3643            new_ast,
3644            sketch_id,
3645            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3646        )?;
3647        Ok(sketch_block_ref)
3648    }
3649
3650    async fn add_distance(
3651        &mut self,
3652        sketch: ObjectId,
3653        distance: Distance,
3654        new_ast: &mut ast::Node<ast::Program>,
3655    ) -> Result<AstNodeRef, KclError> {
3656        let sketch_id = sketch;
3657        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3658            [pt0, pt1] => [
3659                self.coincident_segment_to_ast(pt0, new_ast)?,
3660                self.coincident_segment_to_ast(pt1, new_ast)?,
3661            ],
3662            _ => {
3663                return Err(KclError::refactor(format!(
3664                    "Distance constraint must have exactly 2 points, got {}",
3665                    distance.points.len()
3666                )));
3667            }
3668        };
3669
3670        let arguments = match &distance.label_position {
3671            Some(label_position) => vec![ast::LabeledArg {
3672                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3673                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3674            }],
3675            None => Default::default(),
3676        };
3677
3678        // Create the distance() call.
3679        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3680            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3681            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3682                ast::ArrayExpression {
3683                    elements: vec![pt0_ast, pt1_ast],
3684                    digest: None,
3685                    non_code_meta: Default::default(),
3686                },
3687            )))),
3688            arguments,
3689            digest: None,
3690            non_code_meta: Default::default(),
3691        })));
3692        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3693            left: distance_call_ast,
3694            operator: ast::BinaryOperator::Eq,
3695            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3696                value: ast::LiteralValue::Number {
3697                    value: distance.distance.value,
3698                    suffix: distance.distance.units,
3699                },
3700                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3701                    KclError::refactor(format!(
3702                        "Could not format numeric suffix: {:?}",
3703                        distance.distance.units
3704                    ))
3705                })?,
3706                digest: None,
3707            }))),
3708            digest: None,
3709        })));
3710
3711        // Add the line to the AST of the sketch block.
3712        let (sketch_block_ref, _) = self.mutate_ast(
3713            new_ast,
3714            sketch_id,
3715            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3716        )?;
3717        Ok(sketch_block_ref)
3718    }
3719
3720    async fn add_angle(
3721        &mut self,
3722        sketch: ObjectId,
3723        angle: Angle,
3724        new_ast: &mut ast::Node<ast::Program>,
3725    ) -> Result<AstNodeRef, KclError> {
3726        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3727            return Err(KclError::refactor(format!(
3728                "Angle constraint must have exactly 2 lines, got {}",
3729                angle.lines.len()
3730            )));
3731        };
3732        let sketch_id = sketch;
3733
3734        // Map the runtime objects back to variable names.
3735        let line0_object = self
3736            .scene_graph
3737            .objects
3738            .get(l0_id.0)
3739            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3740        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3741            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3742        };
3743        let Segment::Line(_) = line0_segment else {
3744            return Err(KclError::refactor(format!(
3745                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3746            )));
3747        };
3748        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3749
3750        let line1_object = self
3751            .scene_graph
3752            .objects
3753            .get(l1_id.0)
3754            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3755        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3756            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3757        };
3758        let Segment::Line(_) = line1_segment else {
3759            return Err(KclError::refactor(format!(
3760                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3761            )));
3762        };
3763        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3764
3765        // Create the angle() call.
3766        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3767            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3768            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3769                ast::ArrayExpression {
3770                    elements: vec![l0_ast, l1_ast],
3771                    digest: None,
3772                    non_code_meta: Default::default(),
3773                },
3774            )))),
3775            arguments: Default::default(),
3776            digest: None,
3777            non_code_meta: Default::default(),
3778        })));
3779        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3780            left: angle_call_ast,
3781            operator: ast::BinaryOperator::Eq,
3782            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3783                value: ast::LiteralValue::Number {
3784                    value: angle.angle.value,
3785                    suffix: angle.angle.units,
3786                },
3787                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3788                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3789                })?,
3790                digest: None,
3791            }))),
3792            digest: None,
3793        })));
3794
3795        // Add the line to the AST of the sketch block.
3796        let (sketch_block_ref, _) = self.mutate_ast(
3797            new_ast,
3798            sketch_id,
3799            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3800        )?;
3801        Ok(sketch_block_ref)
3802    }
3803
3804    async fn add_tangent(
3805        &mut self,
3806        sketch: ObjectId,
3807        tangent: Tangent,
3808        new_ast: &mut ast::Node<ast::Program>,
3809    ) -> Result<AstNodeRef, KclError> {
3810        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3811            return Err(KclError::refactor(format!(
3812                "Tangent constraint must have exactly 2 segments, got {}",
3813                tangent.input.len()
3814            )));
3815        };
3816        let sketch_id = sketch;
3817
3818        let seg0_object = self
3819            .scene_graph
3820            .objects
3821            .get(seg0_id.0)
3822            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3823        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3824            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3825        };
3826        let seg0_ast = match seg0_segment {
3827            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3828                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3829            }
3830            _ => {
3831                return Err(KclError::refactor(format!(
3832                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3833                )));
3834            }
3835        };
3836
3837        let seg1_object = self
3838            .scene_graph
3839            .objects
3840            .get(seg1_id.0)
3841            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3842        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3843            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3844        };
3845        let seg1_ast = match seg1_segment {
3846            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3847                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3848            }
3849            _ => {
3850                return Err(KclError::refactor(format!(
3851                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3852                )));
3853            }
3854        };
3855
3856        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3857        let (sketch_block_ref, _) = self.mutate_ast(
3858            new_ast,
3859            sketch_id,
3860            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3861        )?;
3862        Ok(sketch_block_ref)
3863    }
3864
3865    async fn add_symmetric(
3866        &mut self,
3867        sketch: ObjectId,
3868        symmetric: Symmetric,
3869        new_ast: &mut ast::Node<ast::Program>,
3870    ) -> Result<AstNodeRef, KclError> {
3871        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3872            return Err(KclError::refactor(format!(
3873                "Symmetric constraint must have exactly 2 inputs, got {}",
3874                symmetric.input.len()
3875            )));
3876        };
3877        let sketch_id = sketch;
3878
3879        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3880        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3881        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3882
3883        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3884        let (sketch_block_ref, _) = self.mutate_ast(
3885            new_ast,
3886            sketch_id,
3887            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3888        )?;
3889        Ok(sketch_block_ref)
3890    }
3891
3892    async fn add_midpoint(
3893        &mut self,
3894        sketch: ObjectId,
3895        midpoint: Midpoint,
3896        new_ast: &mut ast::Node<ast::Program>,
3897    ) -> Result<AstNodeRef, KclError> {
3898        let sketch_id = sketch;
3899        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3900
3901        let segment_object = self
3902            .scene_graph
3903            .objects
3904            .get(midpoint.segment.0)
3905            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3906        let ObjectKind::Segment {
3907            segment: midpoint_segment,
3908        } = &segment_object.kind
3909        else {
3910            return Err(KclError::refactor(format!(
3911                "Object must be a segment, but it was {}",
3912                segment_object.kind.human_friendly_kind_with_article()
3913            )));
3914        };
3915        let segment_ast = match midpoint_segment {
3916            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3917            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3918            _ => {
3919                return Err(KclError::refactor(format!(
3920                    "Midpoint target must be a line or arc segment but it was {}",
3921                    midpoint_segment.human_friendly_kind_with_article()
3922                )));
3923            }
3924        };
3925
3926        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3927        let (sketch_block_ref, _) = self.mutate_ast(
3928            new_ast,
3929            sketch_id,
3930            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3931        )?;
3932        Ok(sketch_block_ref)
3933    }
3934
3935    async fn add_equal_radius(
3936        &mut self,
3937        sketch: ObjectId,
3938        equal_radius: EqualRadius,
3939        new_ast: &mut ast::Node<ast::Program>,
3940    ) -> Result<AstNodeRef, KclError> {
3941        if equal_radius.input.len() < 2 {
3942            return Err(KclError::refactor(format!(
3943                "equalRadius constraint must have at least 2 segments, got {}",
3944                equal_radius.input.len()
3945            )));
3946        }
3947
3948        let sketch_id = sketch;
3949        let input_asts = equal_radius
3950            .input
3951            .iter()
3952            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3953            .collect::<Result<Vec<_>, _>>()?;
3954
3955        let equal_radius_ast = create_equal_radius_ast(input_asts);
3956        let (sketch_block_ref, _) = self.mutate_ast(
3957            new_ast,
3958            sketch_id,
3959            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3960        )?;
3961        Ok(sketch_block_ref)
3962    }
3963
3964    async fn add_radius(
3965        &mut self,
3966        sketch: ObjectId,
3967        radius: Radius,
3968        new_ast: &mut ast::Node<ast::Program>,
3969    ) -> Result<AstNodeRef, KclError> {
3970        let params = ArcSizeConstraintParams {
3971            points: vec![radius.arc],
3972            function_name: RADIUS_FN,
3973            value: radius.radius.value,
3974            units: radius.radius.units,
3975            label_position: radius.label_position,
3976            constraint_type_name: "Radius",
3977        };
3978        self.add_arc_size_constraint(sketch, params, new_ast).await
3979    }
3980
3981    async fn add_diameter(
3982        &mut self,
3983        sketch: ObjectId,
3984        diameter: Diameter,
3985        new_ast: &mut ast::Node<ast::Program>,
3986    ) -> Result<AstNodeRef, KclError> {
3987        let params = ArcSizeConstraintParams {
3988            points: vec![diameter.arc],
3989            function_name: DIAMETER_FN,
3990            value: diameter.diameter.value,
3991            units: diameter.diameter.units,
3992            label_position: diameter.label_position,
3993            constraint_type_name: "Diameter",
3994        };
3995        self.add_arc_size_constraint(sketch, params, new_ast).await
3996    }
3997
3998    async fn add_fixed_constraints(
3999        &mut self,
4000        sketch: ObjectId,
4001        points: Vec<FixedPoint>,
4002        new_ast: &mut ast::Node<ast::Program>,
4003    ) -> Result<AstNodeRef, KclError> {
4004        let mut sketch_block_ref = None;
4005
4006        for fixed_point in points {
4007            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4008            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4009                .map_err(|err| KclError::refactor(err.to_string()))?;
4010
4011            let (sketch_ref, _) = self.mutate_ast(
4012                new_ast,
4013                sketch,
4014                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4015            )?;
4016            sketch_block_ref = Some(sketch_ref);
4017        }
4018
4019        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4020    }
4021
4022    async fn add_arc_size_constraint(
4023        &mut self,
4024        sketch: ObjectId,
4025        params: ArcSizeConstraintParams,
4026        new_ast: &mut ast::Node<ast::Program>,
4027    ) -> Result<AstNodeRef, KclError> {
4028        let sketch_id = sketch;
4029
4030        // Constraint must have exactly 1 argument (arc segment)
4031        if params.points.len() != 1 {
4032            return Err(KclError::refactor(format!(
4033                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4034                params.constraint_type_name,
4035                params.points.len()
4036            )));
4037        }
4038
4039        let arc_id = params.points[0];
4040        let arc_object = self
4041            .scene_graph
4042            .objects
4043            .get(arc_id.0)
4044            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4045        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4046            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4047        };
4048        let ref_type = match arc_segment {
4049            Segment::Arc(_) => ARC_VARIABLE,
4050            Segment::Circle(_) => CIRCLE_VARIABLE,
4051            _ => {
4052                return Err(KclError::refactor(format!(
4053                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4054                    params.constraint_type_name
4055                )));
4056            }
4057        };
4058        // Reference the arc/circle segment directly
4059        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4060        let arguments = match &params.label_position {
4061            Some(label_position) => vec![ast::LabeledArg {
4062                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4063                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4064            }],
4065            None => Default::default(),
4066        };
4067
4068        // Create the function call.
4069        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4070            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4071            unlabeled: Some(arc_ast),
4072            arguments,
4073            digest: None,
4074            non_code_meta: Default::default(),
4075        })));
4076        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4077            left: call_ast,
4078            operator: ast::BinaryOperator::Eq,
4079            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4080                value: ast::LiteralValue::Number {
4081                    value: params.value,
4082                    suffix: params.units,
4083                },
4084                raw: format_number_literal(params.value, params.units, None)
4085                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4086                digest: None,
4087            }))),
4088            digest: None,
4089        })));
4090
4091        // Add the line to the AST of the sketch block.
4092        let (sketch_block_ref, _) = self.mutate_ast(
4093            new_ast,
4094            sketch_id,
4095            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4096        )?;
4097        Ok(sketch_block_ref)
4098    }
4099
4100    async fn add_horizontal_distance(
4101        &mut self,
4102        sketch: ObjectId,
4103        distance: Distance,
4104        new_ast: &mut ast::Node<ast::Program>,
4105    ) -> Result<AstNodeRef, KclError> {
4106        let sketch_id = sketch;
4107        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4108            [pt0, pt1] => [
4109                self.coincident_segment_to_ast(pt0, new_ast)?,
4110                self.coincident_segment_to_ast(pt1, new_ast)?,
4111            ],
4112            _ => {
4113                return Err(KclError::refactor(format!(
4114                    "Horizontal distance constraint must have exactly 2 points, got {}",
4115                    distance.points.len()
4116                )));
4117            }
4118        };
4119
4120        let arguments = match &distance.label_position {
4121            Some(label_position) => vec![ast::LabeledArg {
4122                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4123                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4124            }],
4125            None => Default::default(),
4126        };
4127
4128        // Create the horizontalDistance() call.
4129        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4130            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4131            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4132                ast::ArrayExpression {
4133                    elements: vec![pt0_ast, pt1_ast],
4134                    digest: None,
4135                    non_code_meta: Default::default(),
4136                },
4137            )))),
4138            arguments,
4139            digest: None,
4140            non_code_meta: Default::default(),
4141        })));
4142        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4143            left: distance_call_ast,
4144            operator: ast::BinaryOperator::Eq,
4145            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4146                value: ast::LiteralValue::Number {
4147                    value: distance.distance.value,
4148                    suffix: distance.distance.units,
4149                },
4150                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4151                    KclError::refactor(format!(
4152                        "Could not format numeric suffix: {:?}",
4153                        distance.distance.units
4154                    ))
4155                })?,
4156                digest: None,
4157            }))),
4158            digest: None,
4159        })));
4160
4161        // Add the line to the AST of the sketch block.
4162        let (sketch_block_ref, _) = self.mutate_ast(
4163            new_ast,
4164            sketch_id,
4165            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4166        )?;
4167        Ok(sketch_block_ref)
4168    }
4169
4170    async fn add_vertical_distance(
4171        &mut self,
4172        sketch: ObjectId,
4173        distance: Distance,
4174        new_ast: &mut ast::Node<ast::Program>,
4175    ) -> Result<AstNodeRef, KclError> {
4176        let sketch_id = sketch;
4177        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4178            [pt0, pt1] => [
4179                self.coincident_segment_to_ast(pt0, new_ast)?,
4180                self.coincident_segment_to_ast(pt1, new_ast)?,
4181            ],
4182            _ => {
4183                return Err(KclError::refactor(format!(
4184                    "Vertical distance constraint must have exactly 2 points, got {}",
4185                    distance.points.len()
4186                )));
4187            }
4188        };
4189
4190        let arguments = match &distance.label_position {
4191            Some(label_position) => vec![ast::LabeledArg {
4192                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4193                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4194            }],
4195            None => Default::default(),
4196        };
4197
4198        // Create the verticalDistance() call.
4199        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4200            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4201            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4202                ast::ArrayExpression {
4203                    elements: vec![pt0_ast, pt1_ast],
4204                    digest: None,
4205                    non_code_meta: Default::default(),
4206                },
4207            )))),
4208            arguments,
4209            digest: None,
4210            non_code_meta: Default::default(),
4211        })));
4212        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4213            left: distance_call_ast,
4214            operator: ast::BinaryOperator::Eq,
4215            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4216                value: ast::LiteralValue::Number {
4217                    value: distance.distance.value,
4218                    suffix: distance.distance.units,
4219                },
4220                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4221                    KclError::refactor(format!(
4222                        "Could not format numeric suffix: {:?}",
4223                        distance.distance.units
4224                    ))
4225                })?,
4226                digest: None,
4227            }))),
4228            digest: None,
4229        })));
4230
4231        // Add the line to the AST of the sketch block.
4232        let (sketch_block_ref, _) = self.mutate_ast(
4233            new_ast,
4234            sketch_id,
4235            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4236        )?;
4237        Ok(sketch_block_ref)
4238    }
4239
4240    async fn add_horizontal(
4241        &mut self,
4242        sketch: ObjectId,
4243        horizontal: Horizontal,
4244        new_ast: &mut ast::Node<ast::Program>,
4245    ) -> Result<AstNodeRef, KclError> {
4246        let sketch_id = sketch;
4247
4248        // Map the runtime objects back to variable names.
4249        let first_arg_ast = match horizontal {
4250            Horizontal::Line { line } => {
4251                let line_object = self
4252                    .scene_graph
4253                    .objects
4254                    .get(line.0)
4255                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4256                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4257                    let kind = line_object.kind.human_friendly_kind_with_article();
4258                    return Err(KclError::refactor(format!(
4259                        "This constraint only works on Segments, but you selected {kind}"
4260                    )));
4261                };
4262                let Segment::Line(_) = line_segment else {
4263                    return Err(KclError::refactor(format!(
4264                        "Only lines can be made horizontal, but you selected {}",
4265                        line_segment.human_friendly_kind_with_article(),
4266                    )));
4267                };
4268                self.line_id_to_ast_reference(line, new_ast)?
4269            }
4270            Horizontal::Points { points } => {
4271                let point_asts = points
4272                    .iter()
4273                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4274                    .collect::<Result<Vec<_>, _>>()?;
4275                ast::ArrayExpression::new(point_asts).into()
4276            }
4277        };
4278        // Create the horizontal() call using shared helper.
4279        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4280
4281        // Add the line to the AST of the sketch block.
4282        let (sketch_block_ref, _) = self.mutate_ast(
4283            new_ast,
4284            sketch_id,
4285            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4286        )?;
4287        Ok(sketch_block_ref)
4288    }
4289
4290    async fn add_lines_equal_length(
4291        &mut self,
4292        sketch: ObjectId,
4293        lines_equal_length: LinesEqualLength,
4294        new_ast: &mut ast::Node<ast::Program>,
4295    ) -> Result<AstNodeRef, KclError> {
4296        if lines_equal_length.lines.len() < 2 {
4297            return Err(KclError::refactor(format!(
4298                "Lines equal length constraint must have at least 2 lines, got {}",
4299                lines_equal_length.lines.len()
4300            )));
4301        };
4302
4303        let sketch_id = sketch;
4304
4305        // Map the runtime objects back to variable names.
4306        let line_asts = lines_equal_length
4307            .lines
4308            .iter()
4309            .map(|line_id| {
4310                let line_object = self
4311                    .scene_graph
4312                    .objects
4313                    .get(line_id.0)
4314                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4315                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4316                    let kind = line_object.kind.human_friendly_kind_with_article();
4317                    return Err(KclError::refactor(format!(
4318                        "This constraint only works on Segments, but you selected {kind}"
4319                    )));
4320                };
4321                let Segment::Line(_) = line_segment else {
4322                    let kind = line_segment.human_friendly_kind_with_article();
4323                    return Err(KclError::refactor(format!(
4324                        "Only lines can be made equal length, but you selected {kind}"
4325                    )));
4326                };
4327
4328                self.line_id_to_ast_reference(*line_id, new_ast)
4329            })
4330            .collect::<Result<Vec<_>, _>>()?;
4331
4332        // Create the equalLength() call using shared helper.
4333        let equal_length_ast = create_equal_length_ast(line_asts);
4334
4335        // Add the constraint to the AST of the sketch block.
4336        let (sketch_block_ref, _) = self.mutate_ast(
4337            new_ast,
4338            sketch_id,
4339            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4340        )?;
4341        Ok(sketch_block_ref)
4342    }
4343
4344    fn equal_radius_segment_id_to_ast_reference(
4345        &mut self,
4346        segment_id: ObjectId,
4347        new_ast: &mut ast::Node<ast::Program>,
4348    ) -> Result<ast::Expr, KclError> {
4349        let segment_object = self
4350            .scene_graph
4351            .objects
4352            .get(segment_id.0)
4353            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4354        let ObjectKind::Segment { segment } = &segment_object.kind else {
4355            return Err(KclError::refactor(format!(
4356                "Object is not a segment, it was {}",
4357                segment_object.kind.human_friendly_kind_with_article()
4358            )));
4359        };
4360
4361        let ref_type = match segment {
4362            Segment::Arc(_) => ARC_VARIABLE,
4363            Segment::Circle(_) => CIRCLE_VARIABLE,
4364            _ => {
4365                return Err(KclError::refactor(format!(
4366                    "equalRadius supports only arc/circle segments, got {}",
4367                    segment.human_friendly_kind_with_article()
4368                )));
4369            }
4370        };
4371
4372        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4373    }
4374
4375    fn symmetric_input_id_to_ast_reference(
4376        &mut self,
4377        segment_id: ObjectId,
4378        new_ast: &mut ast::Node<ast::Program>,
4379    ) -> Result<ast::Expr, KclError> {
4380        let segment_object = self
4381            .scene_graph
4382            .objects
4383            .get(segment_id.0)
4384            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4385        let ObjectKind::Segment { segment } = &segment_object.kind else {
4386            return Err(KclError::refactor(format!(
4387                "Object is not a segment, it was {}",
4388                segment_object.kind.human_friendly_kind_with_article()
4389            )));
4390        };
4391
4392        match segment {
4393            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4394            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4395            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4396            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4397            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4398                "Symmetric does not yet support control point splines".to_owned(),
4399            )),
4400        }
4401    }
4402
4403    fn symmetric_axis_id_to_ast_reference(
4404        &mut self,
4405        segment_id: ObjectId,
4406        new_ast: &mut ast::Node<ast::Program>,
4407    ) -> Result<ast::Expr, KclError> {
4408        let segment_object = self
4409            .scene_graph
4410            .objects
4411            .get(segment_id.0)
4412            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4413        let ObjectKind::Segment { segment } = &segment_object.kind else {
4414            return Err(KclError::refactor(format!(
4415                "Object is not a segment, it was {}",
4416                segment_object.kind.human_friendly_kind_with_article()
4417            )));
4418        };
4419        match segment {
4420            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4421            _ => Err(KclError::refactor(format!(
4422                "Symmetric axis must be a line, got {}",
4423                segment.human_friendly_kind_with_article()
4424            ))),
4425        }
4426    }
4427
4428    async fn add_parallel(
4429        &mut self,
4430        sketch: ObjectId,
4431        parallel: Parallel,
4432        new_ast: &mut ast::Node<ast::Program>,
4433    ) -> Result<AstNodeRef, KclError> {
4434        if parallel.lines.len() < 2 {
4435            return Err(KclError::refactor(format!(
4436                "Parallel constraint must have at least 2 lines, got {}",
4437                parallel.lines.len()
4438            )));
4439        };
4440
4441        let sketch_id = sketch;
4442
4443        let line_asts = parallel
4444            .lines
4445            .iter()
4446            .map(|line_id| {
4447                let line_object = self
4448                    .scene_graph
4449                    .objects
4450                    .get(line_id.0)
4451                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4452                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4453                    let kind = line_object.kind.human_friendly_kind_with_article();
4454                    return Err(KclError::refactor(format!(
4455                        "This constraint only works on Segments, but you selected {kind}"
4456                    )));
4457                };
4458                let Segment::Line(_) = line_segment else {
4459                    let kind = line_segment.human_friendly_kind_with_article();
4460                    return Err(KclError::refactor(format!(
4461                        "Only lines can be made parallel, but you selected {kind}"
4462                    )));
4463                };
4464
4465                self.line_id_to_ast_reference(*line_id, new_ast)
4466            })
4467            .collect::<Result<Vec<_>, _>>()?;
4468
4469        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4470            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4471            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4472                ast::ArrayExpression {
4473                    elements: line_asts,
4474                    digest: None,
4475                    non_code_meta: Default::default(),
4476                },
4477            )))),
4478            arguments: Default::default(),
4479            digest: None,
4480            non_code_meta: Default::default(),
4481        })));
4482
4483        let (sketch_block_ref, _) = self.mutate_ast(
4484            new_ast,
4485            sketch_id,
4486            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4487        )?;
4488        Ok(sketch_block_ref)
4489    }
4490
4491    async fn add_perpendicular(
4492        &mut self,
4493        sketch: ObjectId,
4494        perpendicular: Perpendicular,
4495        new_ast: &mut ast::Node<ast::Program>,
4496    ) -> Result<AstNodeRef, KclError> {
4497        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4498            .await
4499    }
4500
4501    async fn add_lines_at_angle_constraint(
4502        &mut self,
4503        sketch: ObjectId,
4504        angle_kind: LinesAtAngleKind,
4505        lines: Vec<ObjectId>,
4506        new_ast: &mut ast::Node<ast::Program>,
4507    ) -> Result<AstNodeRef, KclError> {
4508        let &[line0_id, line1_id] = lines.as_slice() else {
4509            return Err(KclError::refactor(format!(
4510                "{} constraint must have exactly 2 lines, got {}",
4511                angle_kind.to_function_name(),
4512                lines.len()
4513            )));
4514        };
4515
4516        let sketch_id = sketch;
4517
4518        // Map the runtime objects back to variable names.
4519        let line0_object = self
4520            .scene_graph
4521            .objects
4522            .get(line0_id.0)
4523            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4524        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4525            let kind = line0_object.kind.human_friendly_kind_with_article();
4526            return Err(KclError::refactor(format!(
4527                "This constraint only works on Segments, but you selected {kind}"
4528            )));
4529        };
4530        let Segment::Line(_) = line0_segment else {
4531            return Err(KclError::refactor(format!(
4532                "Only lines can be made {}, but you selected {}",
4533                angle_kind.to_function_name(),
4534                line0_segment.human_friendly_kind_with_article(),
4535            )));
4536        };
4537        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4538
4539        let line1_object = self
4540            .scene_graph
4541            .objects
4542            .get(line1_id.0)
4543            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4544        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4545            let kind = line1_object.kind.human_friendly_kind_with_article();
4546            return Err(KclError::refactor(format!(
4547                "This constraint only works on Segments, but you selected {kind}"
4548            )));
4549        };
4550        let Segment::Line(_) = line1_segment else {
4551            return Err(KclError::refactor(format!(
4552                "Only lines can be made {}, but you selected {}",
4553                angle_kind.to_function_name(),
4554                line1_segment.human_friendly_kind_with_article(),
4555            )));
4556        };
4557        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4558
4559        // Create the parallel() or perpendicular() call.
4560        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4561            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4562            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4563                ast::ArrayExpression {
4564                    elements: vec![line0_ast, line1_ast],
4565                    digest: None,
4566                    non_code_meta: Default::default(),
4567                },
4568            )))),
4569            arguments: Default::default(),
4570            digest: None,
4571            non_code_meta: Default::default(),
4572        })));
4573
4574        // Add the constraint to the AST of the sketch block.
4575        let (sketch_block_ref, _) = self.mutate_ast(
4576            new_ast,
4577            sketch_id,
4578            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4579        )?;
4580        Ok(sketch_block_ref)
4581    }
4582
4583    async fn add_vertical(
4584        &mut self,
4585        sketch: ObjectId,
4586        vertical: Vertical,
4587        new_ast: &mut ast::Node<ast::Program>,
4588    ) -> Result<AstNodeRef, KclError> {
4589        let sketch_id = sketch;
4590
4591        let first_arg_ast = match vertical {
4592            Vertical::Line { line } => {
4593                // Map the runtime objects back to variable names.
4594                let line_object = self
4595                    .scene_graph
4596                    .objects
4597                    .get(line.0)
4598                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4599                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4600                    let kind = line_object.kind.human_friendly_kind_with_article();
4601                    return Err(KclError::refactor(format!(
4602                        "This constraint only works on Segments, but you selected {kind}"
4603                    )));
4604                };
4605                let Segment::Line(_) = line_segment else {
4606                    return Err(KclError::refactor(format!(
4607                        "Only lines can be made vertical, but you selected {}",
4608                        line_segment.human_friendly_kind_with_article()
4609                    )));
4610                };
4611                self.line_id_to_ast_reference(line, new_ast)?
4612            }
4613            Vertical::Points { points } => {
4614                let point_asts = points
4615                    .iter()
4616                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4617                    .collect::<Result<Vec<_>, _>>()?;
4618                ast::ArrayExpression::new(point_asts).into()
4619            }
4620        };
4621        // Create the vertical() call using shared helper.
4622        let vertical_ast = create_vertical_ast(first_arg_ast);
4623
4624        // Add the line to the AST of the sketch block.
4625        let (sketch_block_ref, _) = self.mutate_ast(
4626            new_ast,
4627            sketch_id,
4628            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4629        )?;
4630        Ok(sketch_block_ref)
4631    }
4632
4633    async fn execute_after_add_constraint(
4634        &mut self,
4635        ctx: &ExecutorContext,
4636        sketch_id: ObjectId,
4637        sketch_block_ref: AstNodeRef,
4638        new_ast: &mut ast::Node<ast::Program>,
4639    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4640        // Convert to string source to create real source ranges.
4641        let new_source = source_from_ast(new_ast);
4642        // Parse the new KCL source.
4643        let (new_program, errors) = Program::parse(&new_source)
4644            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4645        if !errors.is_empty() {
4646            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4647                "Error parsing KCL source after adding constraint: {errors:?}"
4648            ))));
4649        }
4650        let Some(new_program) = new_program else {
4651            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4652                "No AST produced after adding constraint".to_string(),
4653            )));
4654        };
4655        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4656            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4657                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4658            )))
4659        })?;
4660
4661        // Truncate after the sketch block for mock execution.
4662        // Use a clone so we don't mutate new_program yet
4663        let mut truncated_program = new_program.clone();
4664        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4665            .map_err(KclErrorWithOutputs::no_outputs)?;
4666
4667        // Execute - if this fails, we haven't modified self yet, so state is safe
4668        let outcome = ctx
4669            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4670            .await?;
4671
4672        let new_object_ids = {
4673            // Extract the constraint ID from the execution outcome using source_range_to_object
4674            let constraint_id = outcome
4675                .source_range_to_object
4676                .get(&constraint_node_ref.range)
4677                .copied()
4678                .ok_or_else(|| {
4679                    KclErrorWithOutputs::from_error_outcome(
4680                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4681                        outcome.clone(),
4682                    )
4683                })?;
4684            vec![constraint_id]
4685        };
4686
4687        // Only now, after all operations succeeded, update self.program.
4688        // This ensures state is only modified if everything succeeds.
4689        self.program = new_program;
4690
4691        // Uses MockConfig::default() which has freedom_analysis: true
4692        let outcome = self.update_state_after_exec(outcome, true);
4693
4694        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4695        let scene_graph_delta = SceneGraphDelta {
4696            new_graph: self.scene_graph_for_ui(),
4697            invalidates_ids: false,
4698            new_objects: new_object_ids,
4699            exec_outcome: outcome,
4700        };
4701        Ok((src_delta, scene_graph_delta))
4702    }
4703
4704    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4705        let commit_failure = || {
4706            KclErrorWithOutputs::from_error_outcome(
4707                KclError::refactor(format!("Could not update KCL after {operation}.")),
4708                outcome.clone(),
4709            )
4710        };
4711
4712        let default_length_unit = self.default_length_unit();
4713        let mut settled_ast = self.program.ast.clone();
4714        let mut committed_solver_value = false;
4715        for (var_range, node_path, value) in &outcome.var_solutions {
4716            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4717                return Err(commit_failure());
4718            };
4719            let new_value = match &lookup {
4720                Some(current_literal) => {
4721                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4722                        continue;
4723                    }
4724                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4725                }
4726                None => {
4727                    // Bare `var` with no initial literal to compare against;
4728                    // always commit, using the module's default length unit as
4729                    // an explicit suffix so the written value carries units.
4730                    Number {
4731                        value: number_value_in_default_length_units(*value, default_length_unit),
4732                        units: default_length_unit.into(),
4733                    }
4734                }
4735            };
4736            committed_solver_value = true;
4737            let source_ref = SourceRef::Simple {
4738                range: *var_range,
4739                node_path: node_path.clone(),
4740            };
4741            mutate_ast_node_by_source_ref(
4742                &mut settled_ast,
4743                &source_ref,
4744                AstMutateCommand::EditVarInitialValue { value: new_value },
4745            )
4746            .map_err(|_| commit_failure())?;
4747        }
4748
4749        if !committed_solver_value {
4750            return Ok(SourceDelta {
4751                text: self.program.original_file_contents.clone(),
4752            });
4753        }
4754
4755        let settled_source = source_from_ast(&settled_ast);
4756        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4757        if !errors.is_empty() {
4758            return Err(commit_failure());
4759        }
4760        let Some(settled_program) = settled_program else {
4761            return Err(commit_failure());
4762        };
4763
4764        self.program = settled_program;
4765
4766        Ok(SourceDelta { text: settled_source })
4767    }
4768
4769    // Find constraints that reference the given segments.
4770    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4771        if segment_ids_set.contains(&segment_id) {
4772            return true;
4773        }
4774
4775        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4776            return false;
4777        };
4778        let ObjectKind::Segment { segment } = &segment_object.kind else {
4779            return false;
4780        };
4781        let Segment::Point(point) = segment else {
4782            return false;
4783        };
4784
4785        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4786    }
4787
4788    fn remaining_constraint_segments(
4789        &self,
4790        segments: &[ConstraintSegment],
4791        segment_ids_set: &AhashIndexSet<ObjectId>,
4792    ) -> Vec<ConstraintSegment> {
4793        segments
4794            .iter()
4795            .copied()
4796            .filter(|segment| match segment {
4797                ConstraintSegment::Origin(_) => true,
4798                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4799            })
4800            .collect()
4801    }
4802
4803    fn find_referenced_constraints(
4804        &self,
4805        sketch_id: ObjectId,
4806        segment_ids_set: &AhashIndexSet<ObjectId>,
4807    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4808        // Look up the sketch.
4809        let sketch_object = self
4810            .scene_graph
4811            .objects
4812            .get(sketch_id.0)
4813            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4814        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4815            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4816        };
4817        let segment_or_owner_matches = |segment_id: ObjectId| {
4818            if segment_ids_set.contains(&segment_id) {
4819                return true;
4820            }
4821            let segment_object = self.scene_graph.objects.get(segment_id.0);
4822            if let Some(obj) = segment_object
4823                && let ObjectKind::Segment { segment } = &obj.kind
4824            {
4825                match segment {
4826                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4827                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4828                    _ => false,
4829                }
4830            } else {
4831                false
4832            }
4833        };
4834        let mut constraint_ids_set = AhashIndexSet::default();
4835        for constraint_id in &sketch.constraints {
4836            let constraint_object = self
4837                .scene_graph
4838                .objects
4839                .get(constraint_id.0)
4840                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4841            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4842                return Err(KclError::refactor(format!(
4843                    "Object is not a constraint, it is {}",
4844                    constraint_object.kind.human_friendly_kind_with_article()
4845                )));
4846            };
4847            let depends_on_segment = match constraint {
4848                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4849                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4850                Constraint::Fixed(fixed) => fixed
4851                    .points
4852                    .iter()
4853                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4854                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4855                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4856                Constraint::EqualRadius(equal_radius) => {
4857                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4858                }
4859                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4860                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4861                Constraint::Horizontal(h) => match h {
4862                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4863                    Horizontal::Points { points } => points.iter().any(|point| match point {
4864                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4865                        ConstraintSegment::Origin(_) => false,
4866                    }),
4867                },
4868                Constraint::Vertical(v) => match v {
4869                    Vertical::Line { line } => segment_or_owner_matches(*line),
4870                    Vertical::Points { points } => points.iter().any(|point| match point {
4871                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872                        ConstraintSegment::Origin(_) => false,
4873                    }),
4874                },
4875                Constraint::LinesEqualLength(lines_equal_length) => {
4876                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4877                }
4878                Constraint::Midpoint(midpoint) => {
4879                    segment_or_owner_matches(midpoint.segment)
4880                        || matches!(
4881                            midpoint.point,
4882                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4883                        )
4884                }
4885                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4886                Constraint::Perpendicular(perpendicular) => {
4887                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4888                }
4889                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4890                Constraint::Symmetric(symmetric) => {
4891                    segment_or_owner_matches(symmetric.axis)
4892                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4893                }
4894                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4895            };
4896            if depends_on_segment {
4897                constraint_ids_set.insert(*constraint_id);
4898            }
4899        }
4900        Ok(constraint_ids_set)
4901    }
4902
4903    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4904        let mut outcome = outcome;
4905        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4906
4907        if freedom_analysis_ran {
4908            // When freedom analysis ran, replace the cache entirely with new values
4909            // Don't merge with old values since IDs might have changed
4910            self.point_freedom_cache.clear();
4911            for new_obj in &new_objects {
4912                if let ObjectKind::Segment {
4913                    segment: crate::front::Segment::Point(point),
4914                } = &new_obj.kind
4915                {
4916                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4917                }
4918            }
4919            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4920            // Objects are already correct from the analysis, just use them as-is
4921            self.scene_graph.objects = new_objects;
4922        } else {
4923            // When freedom analysis didn't run, preserve old values and merge
4924            // Before replacing objects, extract and store freedom values from old objects
4925            for old_obj in &self.scene_graph.objects {
4926                if let ObjectKind::Segment {
4927                    segment: crate::front::Segment::Point(point),
4928                } = &old_obj.kind
4929                {
4930                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4931                }
4932            }
4933
4934            // Update objects, preserving stored freedom values when new is Free (might be default)
4935            let mut updated_objects = Vec::with_capacity(new_objects.len());
4936            for new_obj in new_objects {
4937                let mut obj = new_obj;
4938                if let ObjectKind::Segment {
4939                    segment: crate::front::Segment::Point(point),
4940                } = &mut obj.kind
4941                {
4942                    let new_freedom = point.freedom;
4943                    // When freedom_analysis=false, new values are defaults (Free).
4944                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4945                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4946                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4947                    // when there are actually unsatisfied constraints.
4948                    match new_freedom {
4949                        Freedom::Free => {
4950                            match self.point_freedom_cache.get(&obj.id).copied() {
4951                                Some(Freedom::Conflict) => {
4952                                    // Don't preserve Conflict - conflicts are transient
4953                                    // Keep it as Free
4954                                }
4955                                Some(Freedom::Fixed) => {
4956                                    // Preserve Fixed cached value
4957                                    point.freedom = Freedom::Fixed;
4958                                }
4959                                Some(Freedom::Free) => {
4960                                    // If stored is also Free, keep Free (no change needed)
4961                                }
4962                                None => {
4963                                    // If no cached value, keep Free (default)
4964                                }
4965                            }
4966                        }
4967                        Freedom::Fixed => {
4968                            // Use new value (already set)
4969                        }
4970                        Freedom::Conflict => {
4971                            // Use new value (already set)
4972                        }
4973                    }
4974                    // Store the new freedom value (even if it's Free, so we know it was set)
4975                    self.point_freedom_cache.insert(obj.id, point.freedom);
4976                }
4977                updated_objects.push(obj);
4978            }
4979
4980            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4981            self.scene_graph.objects = updated_objects;
4982        }
4983        outcome
4984    }
4985
4986    fn mutate_ast(
4987        &mut self,
4988        ast: &mut ast::Node<ast::Program>,
4989        object_id: ObjectId,
4990        command: AstMutateCommand,
4991    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4992        let sketch_object = self
4993            .scene_graph
4994            .objects
4995            .get(object_id.0)
4996            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4997        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4998    }
4999}
5000
5001fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5002    // Look up existing sketch.
5003    let sketch_object = scene_graph
5004        .objects
5005        .get(sketch_id.0)
5006        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5007    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5008        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5009    };
5010    expect_single_node_ref(sketch_object)
5011}
5012
5013fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5014    match &object.source {
5015        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5016            range: *range,
5017            node_path: node_path.clone(),
5018        }),
5019        SourceRef::BackTrace { ranges } => {
5020            let [range] = ranges.as_slice() else {
5021                return Err(KclError::refactor(format!(
5022                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5023                    ranges.len()
5024                )));
5025            };
5026            Ok(AstNodeRef {
5027                range: range.0,
5028                node_path: range.1.clone(),
5029            })
5030        }
5031    }
5032}
5033
5034/// This is a deprecated fall-back implementation. Prefer
5035/// [`only_sketch_block()`] to avoid reliance on source ranges.
5036fn only_sketch_block_from_range(
5037    ast: &mut ast::Node<ast::Program>,
5038    sketch_block_range: SourceRange,
5039    edit_kind: ChangeKind,
5040) -> Result<(), KclError> {
5041    let r1 = sketch_block_range;
5042    let matches_range = |r2: SourceRange| -> bool {
5043        // We may have added items to the sketch block, so the end may not be an
5044        // exact match.
5045        match edit_kind {
5046            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5047            // For edit, we don't know whether it grew or shrank.
5048            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5049            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5050            // No edit should be an exact match.
5051            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5052        }
5053    };
5054    let mut found = false;
5055    for item in ast.body.iter_mut() {
5056        match item {
5057            ast::BodyItem::ImportStatement(_) => {}
5058            ast::BodyItem::ExpressionStatement(node) => {
5059                if matches_range(SourceRange::from(&*node))
5060                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5061                {
5062                    sketch_block.is_being_edited = true;
5063                    found = true;
5064                    break;
5065                }
5066            }
5067            ast::BodyItem::VariableDeclaration(node) => {
5068                if matches_range(SourceRange::from(&node.declaration.init))
5069                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5070                {
5071                    sketch_block.is_being_edited = true;
5072                    found = true;
5073                    break;
5074                }
5075            }
5076            ast::BodyItem::TypeDeclaration(_) => {}
5077            ast::BodyItem::ReturnStatement(node) => {
5078                if matches_range(SourceRange::from(&node.argument))
5079                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5080                {
5081                    sketch_block.is_being_edited = true;
5082                    found = true;
5083                    break;
5084                }
5085            }
5086        }
5087    }
5088    if !found {
5089        return Err(KclError::refactor(format!(
5090            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5091        )));
5092    }
5093
5094    Ok(())
5095}
5096
5097fn only_sketch_block(
5098    ast: &mut ast::Node<ast::Program>,
5099    sketch_block_ref: &AstNodeRef,
5100    edit_kind: ChangeKind,
5101) -> Result<(), KclError> {
5102    let Some(target_node_path) = &sketch_block_ref.node_path else {
5103        #[cfg(target_arch = "wasm32")]
5104        web_sys::console::warn_1(
5105            &format!(
5106                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5107                &sketch_block_ref
5108            )
5109            .into(),
5110        );
5111        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5112    };
5113    let mut found = false;
5114    for item in ast.body.iter_mut() {
5115        match item {
5116            ast::BodyItem::ImportStatement(_) => {}
5117            ast::BodyItem::ExpressionStatement(node) => {
5118                // Check the statement.
5119                if let Some(node_path) = &node.node_path
5120                    && node_path == target_node_path
5121                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5122                {
5123                    sketch_block.is_being_edited = true;
5124                    found = true;
5125                    break;
5126                }
5127                // Check the expression.
5128                if let Some(node_path) = node.expression.node_path()
5129                    && node_path == target_node_path
5130                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5131                {
5132                    sketch_block.is_being_edited = true;
5133                    found = true;
5134                    break;
5135                }
5136            }
5137            ast::BodyItem::VariableDeclaration(node) => {
5138                if let Some(node_path) = node.declaration.init.node_path()
5139                    && node_path == target_node_path
5140                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5141                {
5142                    sketch_block.is_being_edited = true;
5143                    found = true;
5144                    break;
5145                }
5146            }
5147            ast::BodyItem::TypeDeclaration(_) => {}
5148            ast::BodyItem::ReturnStatement(node) => {
5149                if let Some(node_path) = node.argument.node_path()
5150                    && node_path == target_node_path
5151                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5152                {
5153                    sketch_block.is_being_edited = true;
5154                    found = true;
5155                    break;
5156                }
5157            }
5158        }
5159    }
5160    if !found {
5161        return Err(KclError::refactor(format!(
5162            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5163        )));
5164    }
5165
5166    Ok(())
5167}
5168
5169fn sketch_on_ast_expr(
5170    ast: &mut ast::Node<ast::Program>,
5171    scene_graph: &SceneGraph,
5172    on: &Plane,
5173) -> Result<ast::Expr, KclError> {
5174    match on {
5175        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5176        Plane::Object(object_id) => {
5177            let on_object = scene_graph
5178                .objects
5179                .get(object_id.0)
5180                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5181            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5182                return Ok(face_expr);
5183            }
5184            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5185        }
5186    }
5187}
5188
5189fn sketch_face_of_scene_object_ast_expr(
5190    ast: &mut ast::Node<ast::Program>,
5191    on_object: &crate::front::Object,
5192) -> Result<Option<ast::Expr>, KclError> {
5193    let SourceRef::BackTrace { ranges } = &on_object.source else {
5194        return Ok(None);
5195    };
5196
5197    match &on_object.kind {
5198        ObjectKind::Wall(_) => {
5199            let (solid_range, sweep_range, segment_range) = match ranges.as_slice() {
5200                [sweep_range, segment_range] => (sweep_range, sweep_range, segment_range),
5201                [solid_range, sweep_range, segment_range] => (solid_range, sweep_range, segment_range),
5202                _ => {
5203                    return Err(KclError::refactor(format!(
5204                        "Expected wall source metadata to have 2 or 3 ranges, got {}; artifact_id={:?}",
5205                        ranges.len(),
5206                        on_object.artifact_id
5207                    )));
5208                }
5209            };
5210            let solid_ref = get_or_insert_ast_reference(
5211                ast,
5212                &SourceRef::Simple {
5213                    range: solid_range.0,
5214                    node_path: solid_range.1.clone(),
5215                },
5216                "solid",
5217                None,
5218            )?;
5219            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5220                return Err(KclError::refactor(format!(
5221                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5222                    on_object.artifact_id
5223                )));
5224            };
5225            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5226            let sweep_ref = get_or_insert_ast_reference(
5227                ast,
5228                &SourceRef::Simple {
5229                    range: sweep_range.0,
5230                    node_path: sweep_range.1.clone(),
5231                },
5232                "solid",
5233                None,
5234            )?;
5235            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5236                return Err(KclError::refactor(format!(
5237                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5238                    on_object.artifact_id
5239                )));
5240            };
5241            let sweep_name = sweep_name_expr.name.name.clone();
5242            let segment_ref = get_or_insert_ast_reference(
5243                ast,
5244                &SourceRef::Simple {
5245                    range: segment_range.0,
5246                    node_path: segment_range.1.clone(),
5247                },
5248                LINE_VARIABLE,
5249                None,
5250            )?;
5251
5252            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name) {
5253                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5254                    return Err(KclError::refactor(format!(
5255                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5256                        on_object.artifact_id
5257                    )));
5258                };
5259                create_member_expression(
5260                    create_member_expression(ast_name_expr(region_name), "tags"),
5261                    &segment_name_expr.name.name,
5262                )
5263            } else {
5264                segment_ref
5265            };
5266
5267            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5268        }
5269        ObjectKind::Cap(cap) => {
5270            let solid_range = match ranges.as_slice() {
5271                [solid_range] | [solid_range, _] => solid_range,
5272                _ => {
5273                    return Err(KclError::refactor(format!(
5274                        "Expected cap source metadata to have 1 or 2 ranges, got {}; artifact_id={:?}",
5275                        ranges.len(),
5276                        on_object.artifact_id
5277                    )));
5278                }
5279            };
5280            let solid_ref = get_or_insert_ast_reference(
5281                ast,
5282                &SourceRef::Simple {
5283                    range: solid_range.0,
5284                    node_path: solid_range.1.clone(),
5285                },
5286                "solid",
5287                None,
5288            )?;
5289            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5290                return Err(KclError::refactor(format!(
5291                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5292                    on_object.artifact_id
5293                )));
5294            };
5295            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5296            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5297            let face_expr = match cap.kind {
5298                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5299                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5300            };
5301
5302            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5303        }
5304        _ => Ok(None),
5305    }
5306}
5307
5308fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5309    let mut current_id = source_id;
5310    let mut current_composite = None;
5311    let mut visited = HashSet::new();
5312
5313    while visited.insert(current_id) {
5314        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5315
5316        let Some(composite_id) = next_composite_id else {
5317            break;
5318        };
5319        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5320            break;
5321        };
5322
5323        current_id = composite.id;
5324        current_composite = Some(composite);
5325
5326        if !composite.consumed {
5327            break;
5328        }
5329    }
5330
5331    current_composite.map(|composite| &composite.code_ref)
5332}
5333
5334fn downstream_composite_id_for_solid_source(
5335    artifact_graph: &ArtifactGraph,
5336    source_id: ArtifactId,
5337) -> Option<ArtifactId> {
5338    // Source is a path, find its solid.
5339    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5340        && let Some(composite_id) = path.composite_solid_id
5341        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5342        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5343    {
5344        return Some(composite_id);
5345    }
5346
5347    // Source is a sweep, find its path -> then find the solid
5348    for artifact in artifact_graph.values() {
5349        if let Artifact::Path(path) = artifact
5350            && path.sweep_id == Some(source_id)
5351            && let Some(composite_id) = path.composite_solid_id
5352            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5353            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5354        {
5355            return Some(composite_id);
5356        }
5357    }
5358
5359    // Source is a solid, find its downstream solid.
5360    artifact_graph.values().find_map(|artifact| {
5361        let Artifact::CompositeSolid(composite) = artifact else {
5362            return None;
5363        };
5364        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5365    })
5366}
5367
5368fn composite_contains_path_input(
5369    solid_ids: &[ArtifactId],
5370    tool_ids: &[ArtifactId],
5371    path_id: ArtifactId,
5372    solid2d_id: Option<ArtifactId>,
5373) -> bool {
5374    composite_contains_input(solid_ids, tool_ids, path_id)
5375        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5376}
5377
5378fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5379    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5380}
5381
5382fn code_ref_source_ref_range(code_ref: &CodeRef) -> (SourceRange, Option<crate::NodePath>) {
5383    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5384    (code_ref.range, node_path)
5385}
5386
5387fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5388    let mut existing_artifact_ids = scene_objects
5389        .iter()
5390        .map(|object| object.artifact_id)
5391        .collect::<HashSet<_>>();
5392
5393    for artifact in artifact_graph.values() {
5394        match artifact {
5395            Artifact::Wall(wall) => {
5396                if existing_artifact_ids.contains(&wall.id) {
5397                    continue;
5398                }
5399
5400                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5401                    Artifact::Segment(segment) => Some(segment),
5402                    _ => None,
5403                }) else {
5404                    continue;
5405                };
5406                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5407                    Artifact::Sweep(sweep) => Some(sweep),
5408                    _ => None,
5409                }) else {
5410                    continue;
5411                };
5412                let source_segment = segment
5413                    .original_seg_id
5414                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5415                    .and_then(|artifact| match artifact {
5416                        Artifact::Segment(segment) => Some(segment),
5417                        _ => None,
5418                    })
5419                    .unwrap_or(segment);
5420                let solid_code_ref =
5421                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5422                let mut ranges = Vec::new();
5423                if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5424                {
5425                    ranges.push(code_ref_source_ref_range(solid_code_ref));
5426                }
5427                ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5428                ranges.push(code_ref_source_ref_range(&source_segment.code_ref));
5429                let id = ObjectId(scene_objects.len());
5430                scene_objects.push(crate::front::Object {
5431                    id,
5432                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5433                    label: Default::default(),
5434                    comments: Default::default(),
5435                    artifact_id: wall.id,
5436                    source: SourceRef::BackTrace { ranges },
5437                });
5438                existing_artifact_ids.insert(wall.id);
5439            }
5440            Artifact::Cap(cap) => {
5441                if existing_artifact_ids.contains(&cap.id) {
5442                    continue;
5443                }
5444
5445                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5446                    Artifact::Sweep(sweep) => Some(sweep),
5447                    _ => None,
5448                }) else {
5449                    continue;
5450                };
5451                let id = ObjectId(scene_objects.len());
5452                let kind = match cap.sub_type {
5453                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5454                    CapSubType::End => crate::frontend::api::CapKind::End,
5455                };
5456                let solid_code_ref =
5457                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5458                let mut ranges = Vec::new();
5459                if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5460                {
5461                    ranges.push(code_ref_source_ref_range(solid_code_ref));
5462                }
5463                ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5464                scene_objects.push(crate::front::Object {
5465                    id,
5466                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5467                    label: Default::default(),
5468                    comments: Default::default(),
5469                    artifact_id: cap.id,
5470                    source: SourceRef::BackTrace { ranges },
5471                });
5472                existing_artifact_ids.insert(cap.id);
5473            }
5474            _ => {}
5475        }
5476    }
5477}
5478
5479fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5480    use crate::engine::PlaneName;
5481
5482    match name {
5483        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5484        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5485        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5486        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5487        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5488        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5489    }
5490}
5491
5492fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5493    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5494        ast::UnaryOperator::Neg,
5495        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5496    )))
5497}
5498
5499fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5500    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5501        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5502        unlabeled: Some(solid_expr),
5503        arguments: vec![ast::LabeledArg {
5504            label: Some(ast::Identifier::new("face")),
5505            arg: face_expr,
5506        }],
5507        digest: None,
5508        non_code_meta: Default::default(),
5509    })))
5510}
5511
5512fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5513    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5514        return None;
5515    };
5516    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5517        return None;
5518    };
5519    if !matches!(
5520        sweep_call.callee.name.name.as_str(),
5521        "extrude" | "revolve" | "sweep" | "loft"
5522    ) {
5523        return None;
5524    }
5525    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5526        return None;
5527    };
5528    let candidate = region_name_expr.name.name.clone();
5529    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5530        return None;
5531    };
5532    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5533        return None;
5534    };
5535    if region_call.callee.name.name != "region" {
5536        return None;
5537    }
5538    Some(candidate)
5539}
5540
5541/// Return the AST expression referencing the variable at the given source ref.
5542/// If no such variable exists, insert a new variable declaration with the given
5543/// prefix.
5544///
5545/// This may return a complex expression referencing properties of the variable
5546/// (e.g., `line1.start`).
5547fn get_or_insert_ast_reference(
5548    ast: &mut ast::Node<ast::Program>,
5549    source_ref: &SourceRef,
5550    prefix: &str,
5551    property: Option<&str>,
5552) -> Result<ast::Expr, KclError> {
5553    let command = AstMutateCommand::AddVariableDeclaration {
5554        prefix: prefix.to_owned(),
5555    };
5556    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5557        Ok((_, ret)) => ret,
5558        Err(err) => {
5559            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5560                AstMutateCommandReturn::Name(var_name)
5561            } else {
5562                return Err(err);
5563            }
5564        }
5565    };
5566    let AstMutateCommandReturn::Name(var_name) = ret else {
5567        return Err(KclError::refactor(
5568            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5569        ));
5570    };
5571    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5572    let Some(property) = property else {
5573        // No property; just return the variable name.
5574        return Ok(var_expr);
5575    };
5576
5577    Ok(create_member_expression(var_expr, property))
5578}
5579
5580fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5581    let source_range = match source_ref {
5582        SourceRef::Simple { range, .. } => *range,
5583        SourceRef::BackTrace { ranges } => {
5584            let [range] = ranges.as_slice() else {
5585                return None;
5586            };
5587            range.0
5588        }
5589    };
5590    ast.body.iter().find_map(|item| {
5591        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5592            return None;
5593        };
5594        let init_range = SourceRange::from(&var_decl.declaration.init);
5595        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5596            && init_range.start() <= source_range.start()
5597            && source_range.end() <= init_range.end();
5598        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5599            && init_range != source_range
5600            && source_is_inside_init
5601        {
5602            return None;
5603        }
5604        source_is_inside_init.then(|| var_decl.name().to_owned())
5605    })
5606}
5607
5608fn mutate_ast_node_by_source_ref(
5609    ast: &mut ast::Node<ast::Program>,
5610    source_ref: &SourceRef,
5611    command: AstMutateCommand,
5612) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5613    let (source_range, node_path) = match source_ref {
5614        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5615        SourceRef::BackTrace { ranges } => {
5616            let [range] = ranges.as_slice() else {
5617                return Err(KclError::refactor(format!(
5618                    "Expected single source ref, got {}; ranges={ranges:#?}",
5619                    ranges.len(),
5620                )));
5621            };
5622            (range.0, range.1.clone())
5623        }
5624    };
5625    let mut context = AstMutateContext {
5626        source_range,
5627        node_path,
5628        command,
5629        defined_names_stack: Default::default(),
5630    };
5631    let control = dfs_mut(ast, &mut context);
5632    match control {
5633        ControlFlow::Continue(_) => Err(KclError::refactor(
5634            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5635        )),
5636        ControlFlow::Break(break_value) => break_value,
5637    }
5638}
5639
5640#[derive(Debug)]
5641struct AstMutateContext {
5642    source_range: SourceRange,
5643    node_path: Option<ast::NodePath>,
5644    command: AstMutateCommand,
5645    defined_names_stack: Vec<HashSet<String>>,
5646}
5647
5648#[derive(Debug)]
5649#[allow(clippy::large_enum_variant)]
5650enum AstMutateCommand {
5651    /// Add an expression statement to the sketch block.
5652    AddSketchBlockExprStmt {
5653        expr: ast::Expr,
5654    },
5655    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5656    AddSketchBlockVarDecl {
5657        prefix: String,
5658        expr: ast::Expr,
5659    },
5660    AddVariableDeclaration {
5661        prefix: String,
5662    },
5663    EditPoint {
5664        at: ast::Expr,
5665    },
5666    EditLine {
5667        start: ast::Expr,
5668        end: ast::Expr,
5669        construction: Option<bool>,
5670    },
5671    EditArc {
5672        start: ast::Expr,
5673        end: ast::Expr,
5674        center: ast::Expr,
5675        construction: Option<bool>,
5676    },
5677    EditCircle {
5678        start: ast::Expr,
5679        center: ast::Expr,
5680        construction: Option<bool>,
5681    },
5682    EditControlPointSpline {
5683        points: ast::Expr,
5684        construction: Option<bool>,
5685    },
5686    EditConstraintValue {
5687        value: ast::BinaryPart,
5688    },
5689    EditDistanceConstraintLabelPosition {
5690        label_position: ast::Expr,
5691    },
5692    EditCallUnlabeled {
5693        arg: ast::Expr,
5694    },
5695    EditVarInitialValue {
5696        value: Number,
5697    },
5698    DeleteNode,
5699}
5700
5701impl AstMutateCommand {
5702    fn needs_defined_names_stack(&self) -> bool {
5703        matches!(
5704            self,
5705            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5706        )
5707    }
5708}
5709
5710#[derive(Debug)]
5711enum AstMutateCommandReturn {
5712    None,
5713    Name(String),
5714}
5715
5716#[derive(Debug, Clone)]
5717struct AstNodeRef {
5718    range: SourceRange,
5719    node_path: Option<ast::NodePath>,
5720}
5721
5722impl<T> From<&ast::Node<T>> for AstNodeRef {
5723    fn from(value: &ast::Node<T>) -> Self {
5724        AstNodeRef {
5725            range: value.into(),
5726            node_path: value.node_path.clone(),
5727        }
5728    }
5729}
5730
5731impl From<&ast::BodyItem> for AstNodeRef {
5732    fn from(value: &ast::BodyItem) -> Self {
5733        match value {
5734            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5735                range: node.into(),
5736                node_path: node.node_path.clone(),
5737            },
5738            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5739                range: node.into(),
5740                node_path: node.node_path.clone(),
5741            },
5742            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5743                range: node.into(),
5744                node_path: node.node_path.clone(),
5745            },
5746            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5747                range: node.into(),
5748                node_path: node.node_path.clone(),
5749            },
5750            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5751                range: node.into(),
5752                node_path: node.node_path.clone(),
5753            },
5754        }
5755    }
5756}
5757
5758impl From<&ast::Expr> for AstNodeRef {
5759    fn from(value: &ast::Expr) -> Self {
5760        AstNodeRef {
5761            range: SourceRange::from(value),
5762            node_path: value.node_path().cloned(),
5763        }
5764    }
5765}
5766
5767impl From<&AstMutateContext> for AstNodeRef {
5768    fn from(value: &AstMutateContext) -> Self {
5769        AstNodeRef {
5770            range: value.source_range,
5771            node_path: value.node_path.clone(),
5772        }
5773    }
5774}
5775
5776impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5777    type Error = crate::walk::AstNodeError;
5778
5779    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5780        Ok(AstNodeRef {
5781            range: SourceRange::try_from(value)?,
5782            node_path: value.try_into()?,
5783        })
5784    }
5785}
5786
5787impl From<AstNodeRef> for SourceRange {
5788    fn from(value: AstNodeRef) -> Self {
5789        value.range
5790    }
5791}
5792
5793impl Visitor for AstMutateContext {
5794    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5795    type Continue = ();
5796
5797    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5798        filter_and_process(self, node)
5799    }
5800
5801    fn finish(&mut self, node: NodeMut<'_>) {
5802        match &node {
5803            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5804                self.defined_names_stack.pop();
5805            }
5806            _ => {}
5807        }
5808    }
5809}
5810
5811fn filter_and_process(
5812    ctx: &mut AstMutateContext,
5813    node: NodeMut,
5814) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5815    let Ok(node_range) = SourceRange::try_from(&node) else {
5816        // Nodes that can't be converted to a range aren't interesting.
5817        return TraversalReturn::new_continue(());
5818    };
5819    // If we're adding a variable declaration, we need to look at variable
5820    // declaration expressions to see if it already has a variable, before
5821    // continuing. The variable declaration's source range won't match the
5822    // target; its init expression will.
5823    if let NodeMut::VariableDeclaration(var_decl) = &node {
5824        let expr_range = SourceRange::from(&var_decl.declaration.init);
5825        let expr_node_path = var_decl.declaration.init.node_path();
5826        if source_ref_matches(ctx, expr_range, expr_node_path) {
5827            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5828                // We found the variable declaration expression. It doesn't need
5829                // to be added.
5830                return TraversalReturn::new_break(Ok((
5831                    AstNodeRef::from(&**var_decl),
5832                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5833                )));
5834            }
5835            if let AstMutateCommand::DeleteNode = &ctx.command {
5836                // We found the variable declaration. Delete the variable along
5837                // with the segment.
5838                return TraversalReturn {
5839                    mutate_body_item: MutateBodyItem::Delete,
5840                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5841                };
5842            }
5843        }
5844    }
5845    // Similar thing with expression statement. We need to look at the
5846    // expression inside it.
5847    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5848        let expr_range = SourceRange::from(&expr_stmt.expression);
5849        let expr_node_path = expr_stmt.expression.node_path();
5850        if source_ref_matches(ctx, expr_range, expr_node_path) {
5851            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5852                // We found the node wrapped in an expression statement. Process
5853                // the statement.
5854                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5855                    return TraversalReturn::new_continue(());
5856                };
5857                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5858            }
5859            if let AstMutateCommand::DeleteNode = &ctx.command {
5860                // We found the node wrapped in an expression statement. Delete
5861                // the whole statement.
5862                return TraversalReturn {
5863                    mutate_body_item: MutateBodyItem::Delete,
5864                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5865                };
5866            }
5867        }
5868    }
5869
5870    if ctx.command.needs_defined_names_stack() {
5871        if let NodeMut::Program(program) = &node {
5872            ctx.defined_names_stack.push(find_defined_names(*program));
5873        } else if let NodeMut::SketchBlock(block) = &node {
5874            ctx.defined_names_stack.push(find_defined_names(&block.body));
5875        }
5876    }
5877
5878    // Make sure the node matches the source ref.
5879    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5880    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5881        return TraversalReturn::new_continue(());
5882    }
5883    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5884        return TraversalReturn::new_continue(());
5885    };
5886    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5887}
5888
5889fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5890    match &ctx.node_path {
5891        Some(target) => Some(target) == node_path,
5892        None => node_range == ctx.source_range,
5893    }
5894}
5895
5896fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5897    match &ctx.command {
5898        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5899            if let NodeMut::SketchBlock(sketch_block) = node {
5900                sketch_block
5901                    .body
5902                    .items
5903                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5904                        inner: ast::ExpressionStatement {
5905                            expression: expr.clone(),
5906                            digest: None,
5907                        },
5908                        start: Default::default(),
5909                        end: Default::default(),
5910                        module_id: Default::default(),
5911                        node_path: None,
5912                        outer_attrs: Default::default(),
5913                        pre_comments: Default::default(),
5914                        comment_start: Default::default(),
5915                    }));
5916                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5917            }
5918        }
5919        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5920            if let NodeMut::SketchBlock(sketch_block) = node {
5921                let empty_defined_names = HashSet::new();
5922                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5923                let Ok(name) = next_free_name(prefix, defined_names) else {
5924                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5925                };
5926                sketch_block
5927                    .body
5928                    .items
5929                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5930                        ast::VariableDeclaration::new(
5931                            ast::VariableDeclarator::new(&name, expr.clone()),
5932                            ast::ItemVisibility::Default,
5933                            ast::VariableKind::Const,
5934                        ),
5935                    ))));
5936                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5937            }
5938        }
5939        AstMutateCommand::AddVariableDeclaration { prefix } => {
5940            if let NodeMut::VariableDeclaration(inner) = node {
5941                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5942            }
5943            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5944                let empty_defined_names = HashSet::new();
5945                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5946                let Ok(name) = next_free_name(prefix, defined_names) else {
5947                    // TODO: Return an error instead?
5948                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5949                };
5950                let mutate_node =
5951                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5952                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5953                        ast::ItemVisibility::Default,
5954                        ast::VariableKind::Const,
5955                    ))));
5956                return TraversalReturn {
5957                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5958                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5959                };
5960            }
5961        }
5962        AstMutateCommand::EditPoint { at } => {
5963            if let NodeMut::CallExpressionKw(call) = node {
5964                if call.callee.name.name != POINT_FN {
5965                    return TraversalReturn::new_continue(());
5966                }
5967                // Update the arguments.
5968                for labeled_arg in &mut call.arguments {
5969                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5970                        labeled_arg.arg = at.clone();
5971                    }
5972                }
5973                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5974            }
5975        }
5976        AstMutateCommand::EditLine {
5977            start,
5978            end,
5979            construction,
5980        } => {
5981            if let NodeMut::CallExpressionKw(call) = node {
5982                if call.callee.name.name != LINE_FN {
5983                    return TraversalReturn::new_continue(());
5984                }
5985                // Update the arguments.
5986                for labeled_arg in &mut call.arguments {
5987                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5988                        labeled_arg.arg = start.clone();
5989                    }
5990                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5991                        labeled_arg.arg = end.clone();
5992                    }
5993                }
5994                // Handle construction kwarg
5995                if let Some(construction_value) = construction {
5996                    let construction_exists = call
5997                        .arguments
5998                        .iter()
5999                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6000                    if *construction_value {
6001                        // Add or update construction=true
6002                        if construction_exists {
6003                            // Update existing construction kwarg
6004                            for labeled_arg in &mut call.arguments {
6005                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6006                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6007                                        value: ast::LiteralValue::Bool(true),
6008                                        raw: "true".to_string(),
6009                                        digest: None,
6010                                    })));
6011                                }
6012                            }
6013                        } else {
6014                            // Add new construction kwarg
6015                            call.arguments.push(ast::LabeledArg {
6016                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6017                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6018                                    value: ast::LiteralValue::Bool(true),
6019                                    raw: "true".to_string(),
6020                                    digest: None,
6021                                }))),
6022                            });
6023                        }
6024                    } else {
6025                        // Remove construction kwarg if it exists
6026                        call.arguments
6027                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6028                    }
6029                }
6030                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6031            }
6032        }
6033        AstMutateCommand::EditArc {
6034            start,
6035            end,
6036            center,
6037            construction,
6038        } => {
6039            if let NodeMut::CallExpressionKw(call) = node {
6040                if call.callee.name.name != ARC_FN {
6041                    return TraversalReturn::new_continue(());
6042                }
6043                // Update the arguments.
6044                for labeled_arg in &mut call.arguments {
6045                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6046                        labeled_arg.arg = start.clone();
6047                    }
6048                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6049                        labeled_arg.arg = end.clone();
6050                    }
6051                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6052                        labeled_arg.arg = center.clone();
6053                    }
6054                }
6055                // Handle construction kwarg
6056                if let Some(construction_value) = construction {
6057                    let construction_exists = call
6058                        .arguments
6059                        .iter()
6060                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6061                    if *construction_value {
6062                        // Add or update construction=true
6063                        if construction_exists {
6064                            // Update existing construction kwarg
6065                            for labeled_arg in &mut call.arguments {
6066                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6067                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6068                                        value: ast::LiteralValue::Bool(true),
6069                                        raw: "true".to_string(),
6070                                        digest: None,
6071                                    })));
6072                                }
6073                            }
6074                        } else {
6075                            // Add new construction kwarg
6076                            call.arguments.push(ast::LabeledArg {
6077                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6078                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6079                                    value: ast::LiteralValue::Bool(true),
6080                                    raw: "true".to_string(),
6081                                    digest: None,
6082                                }))),
6083                            });
6084                        }
6085                    } else {
6086                        // Remove construction kwarg if it exists
6087                        call.arguments
6088                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6089                    }
6090                }
6091                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6092            }
6093        }
6094        AstMutateCommand::EditCircle {
6095            start,
6096            center,
6097            construction,
6098        } => {
6099            if let NodeMut::CallExpressionKw(call) = node {
6100                if call.callee.name.name != CIRCLE_FN {
6101                    return TraversalReturn::new_continue(());
6102                }
6103                // Update the arguments.
6104                for labeled_arg in &mut call.arguments {
6105                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6106                        labeled_arg.arg = start.clone();
6107                    }
6108                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6109                        labeled_arg.arg = center.clone();
6110                    }
6111                }
6112                // Handle construction kwarg
6113                if let Some(construction_value) = construction {
6114                    let construction_exists = call
6115                        .arguments
6116                        .iter()
6117                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6118                    if *construction_value {
6119                        if construction_exists {
6120                            // Update existing construction kwarg
6121                            for labeled_arg in &mut call.arguments {
6122                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6123                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6124                                        value: ast::LiteralValue::Bool(true),
6125                                        raw: "true".to_string(),
6126                                        digest: None,
6127                                    })));
6128                                }
6129                            }
6130                        } else {
6131                            // Add new construction kwarg
6132                            call.arguments.push(ast::LabeledArg {
6133                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6134                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6135                                    value: ast::LiteralValue::Bool(true),
6136                                    raw: "true".to_string(),
6137                                    digest: None,
6138                                }))),
6139                            });
6140                        }
6141                    } else {
6142                        // Remove construction kwarg if it exists
6143                        call.arguments
6144                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6145                    }
6146                }
6147                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6148            }
6149        }
6150        AstMutateCommand::EditControlPointSpline { points, construction } => {
6151            if let NodeMut::CallExpressionKw(call) = node {
6152                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6153                    return TraversalReturn::new_continue(());
6154                }
6155                for labeled_arg in &mut call.arguments {
6156                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6157                    {
6158                        labeled_arg.arg = points.clone();
6159                    }
6160                }
6161                // Handle construction kwarg
6162                if let Some(construction_value) = construction {
6163                    let construction_exists = call
6164                        .arguments
6165                        .iter()
6166                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6167                    if *construction_value {
6168                        if construction_exists {
6169                            for labeled_arg in &mut call.arguments {
6170                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6171                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6172                                        value: ast::LiteralValue::Bool(true),
6173                                        raw: "true".to_string(),
6174                                        digest: None,
6175                                    })));
6176                                }
6177                            }
6178                        } else {
6179                            call.arguments.push(ast::LabeledArg {
6180                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6181                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6182                                    value: ast::LiteralValue::Bool(true),
6183                                    raw: "true".to_string(),
6184                                    digest: None,
6185                                }))),
6186                            });
6187                        }
6188                    } else {
6189                        call.arguments
6190                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6191                    }
6192                }
6193                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6194            }
6195        }
6196        AstMutateCommand::EditConstraintValue { value } => {
6197            if let NodeMut::BinaryExpression(binary_expr) = node {
6198                let left_is_constraint = matches!(
6199                    &binary_expr.left,
6200                    ast::BinaryPart::CallExpressionKw(call)
6201                        if matches!(
6202                            call.callee.name.name.as_str(),
6203                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6204                        )
6205                );
6206                if left_is_constraint {
6207                    binary_expr.right = value.clone();
6208                } else {
6209                    binary_expr.left = value.clone();
6210                }
6211
6212                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6213            }
6214        }
6215        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6216            if let NodeMut::BinaryExpression(binary_expr) = node {
6217                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6218                    return TraversalReturn::new_continue(());
6219                };
6220                if !matches!(
6221                    call.callee.name.name.as_str(),
6222                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6223                ) {
6224                    return TraversalReturn::new_continue(());
6225                }
6226
6227                if let Some(label_arg) = call
6228                    .arguments
6229                    .iter_mut()
6230                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6231                {
6232                    label_arg.arg = label_position.clone();
6233                } else {
6234                    call.arguments.push(ast::LabeledArg {
6235                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6236                        arg: label_position.clone(),
6237                    });
6238                }
6239
6240                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6241            }
6242        }
6243        AstMutateCommand::EditCallUnlabeled { arg } => {
6244            if let NodeMut::CallExpressionKw(call) = node {
6245                call.unlabeled = Some(arg.clone());
6246                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6247            }
6248        }
6249        AstMutateCommand::EditVarInitialValue { value } => {
6250            // We target the SketchVar itself (matched by NodePath) rather than
6251            // the inner NumericLiteral so we can also write back into vars that
6252            // were declared without an initial value (e.g. bare `var`).
6253            if let NodeMut::SketchVar(sketch_var) = node {
6254                let Ok(literal) = to_source_number(*value) else {
6255                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6256                        "Could not convert number to AST literal: {:?}",
6257                        *value
6258                    ))));
6259                };
6260                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6261                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6262            }
6263        }
6264        AstMutateCommand::DeleteNode => {
6265            return TraversalReturn {
6266                mutate_body_item: MutateBodyItem::Delete,
6267                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6268            };
6269        }
6270    }
6271    TraversalReturn::new_continue(())
6272}
6273
6274struct FindSketchBlockSourceRange {
6275    /// The source range of the sketch block before mutation.
6276    target_before_mutation: SourceRange,
6277    /// The source range of the sketch block's last body item after mutation. We
6278    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6279    /// shared reference.
6280    found: Cell<Option<AstNodeRef>>,
6281}
6282
6283impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6284    type Error = crate::front::Error;
6285
6286    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6287        let Ok(node_range) = SourceRange::try_from(&node) else {
6288            return Ok(true);
6289        };
6290
6291        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6292            if node_range.module_id() == self.target_before_mutation.module_id()
6293                && node_range.start() == self.target_before_mutation.start()
6294                // End shouldn't match since we added something.
6295                && node_range.end() >= self.target_before_mutation.end()
6296            {
6297                self.found.set(sketch_block.body.items.last().map(|item| match item {
6298                    // For declarations like `circle1 = circle(...)`, use
6299                    // the init expression range so lookup in source_range_to_object
6300                    // matches the segment source range.
6301                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6302                    _ => AstNodeRef::from(item),
6303                }));
6304                return Ok(false);
6305            } else {
6306                // We found a different sketch block. No need to descend into
6307                // its children since sketch blocks cannot be nested.
6308                return Ok(true);
6309            }
6310        }
6311
6312        for child in node.children().iter() {
6313            if !child.visit(*self)? {
6314                return Ok(false);
6315            }
6316        }
6317
6318        Ok(true)
6319    }
6320}
6321
6322struct FindSketchBlockByNodePath {
6323    /// The Node Path of the sketch block before mutation.
6324    target_node_path: ast::NodePath,
6325    /// The ref of the sketch block's last body item after mutation. We need to
6326    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6327    /// reference.
6328    found: Cell<Option<AstNodeRef>>,
6329}
6330
6331impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6332    type Error = crate::front::Error;
6333
6334    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6335        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6336            return Ok(true);
6337        };
6338
6339        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6340            if let Some(node_path) = node_path
6341                && node_path == self.target_node_path
6342            {
6343                self.found.set(sketch_block.body.items.last().map(|item| match item {
6344                    // For declarations like `circle1 = circle(...)`, use
6345                    // the init expression range so lookup in source_range_to_object
6346                    // matches the segment source range.
6347                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6348                    _ => AstNodeRef::from(item),
6349                }));
6350
6351                return Ok(false);
6352            } else {
6353                // We found a different sketch block. No need to descend into
6354                // its children since sketch blocks cannot be nested.
6355                return Ok(true);
6356            }
6357        }
6358
6359        for child in node.children().iter() {
6360            if !child.visit(*self)? {
6361                return Ok(false);
6362            }
6363        }
6364
6365        Ok(true)
6366    }
6367}
6368
6369/// After adding an item to a sketch block, find the sketch block, and get the
6370/// source range of the added item. We assume that the added item is the last
6371/// item in the sketch block and that the sketch block's source range has grown,
6372/// but not moved from its starting offset.
6373///
6374/// TODO: Do we need to format *before* mutation in case formatting moves the
6375/// sketch block forward?
6376fn find_sketch_block_added_item(
6377    ast: &ast::Node<ast::Program>,
6378    sketch_block_before_mutation: &AstNodeRef,
6379) -> Result<AstNodeRef, KclError> {
6380    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6381        let find = FindSketchBlockByNodePath {
6382            target_node_path: node_path.clone(),
6383            found: Cell::new(None),
6384        };
6385        let node = crate::walk::Node::from(ast);
6386        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6387        find.found.into_inner().ok_or_else(|| {
6388            KclError::refactor(format!(
6389                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6390            ))
6391        })
6392    } else {
6393        // No NodePath. Fall back to legacy source range.
6394        let find = FindSketchBlockSourceRange {
6395            target_before_mutation: sketch_block_before_mutation.range,
6396            found: Cell::new(None),
6397        };
6398        let node = crate::walk::Node::from(ast);
6399        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6400        find.found.into_inner().ok_or_else(|| KclError::refactor(
6401            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?"),
6402        ))
6403    }
6404}
6405
6406fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6407    // TODO: Don't duplicate this from lib.rs Program.
6408    ast.recast_top(&Default::default(), 0)
6409}
6410
6411struct FindNumericLiteral {
6412    target: SourceRange,
6413    found: Cell<Option<ast::NumericLiteral>>,
6414}
6415
6416impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6417    type Error = crate::front::Error;
6418
6419    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6420        let Ok(node_range) = SourceRange::try_from(&node) else {
6421            return Ok(true);
6422        };
6423
6424        if node_range == self.target
6425            && let crate::walk::Node::NumericLiteral(literal) = node
6426        {
6427            self.found.set(Some(literal.inner.clone()));
6428            return Ok(false);
6429        }
6430
6431        for child in node.children().iter() {
6432            if !child.visit(*self)? {
6433                return Ok(false);
6434            }
6435        }
6436
6437        Ok(true)
6438    }
6439}
6440
6441fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6442    let find = FindNumericLiteral {
6443        target,
6444        found: Cell::new(None),
6445    };
6446    let node = crate::walk::Node::from(ast);
6447    node.visit(&find).ok()?;
6448    find.found.into_inner()
6449}
6450
6451struct FindSketchVarInitialByNodePath<'a> {
6452    target: &'a ast::NodePath,
6453    sketch_var_found: Cell<bool>,
6454    initial_literal: Cell<Option<ast::NumericLiteral>>,
6455}
6456
6457impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6458    type Error = crate::front::Error;
6459
6460    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6461        if let crate::walk::Node::SketchVar(sketch_var) = node
6462            && sketch_var.node_path.as_ref() == Some(self.target)
6463        {
6464            self.sketch_var_found.set(true);
6465            if let Some(initial) = &sketch_var.initial {
6466                self.initial_literal.set(Some(initial.inner.clone()));
6467            }
6468            return Ok(false);
6469        }
6470
6471        for child in node.children().iter() {
6472            if !child.visit(*self)? {
6473                return Ok(false);
6474            }
6475        }
6476
6477        Ok(true)
6478    }
6479}
6480
6481/// Locate the source `var` declaration corresponding to a sketch-var solution.
6482///
6483/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6484/// from "target found." The inner [`Option`] is the initial numeric literal of
6485/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6486///
6487/// When `node_path` is `None` (e.g. for older outcomes that predate the
6488/// node-path propagation), this falls back to source-range matching, which
6489/// can break under whitespace shifts elsewhere in the file.
6490fn numeric_literal_at_node_path(
6491    ast: &ast::Node<ast::Program>,
6492    node_path: Option<&ast::NodePath>,
6493    source_range: SourceRange,
6494) -> Option<Option<ast::NumericLiteral>> {
6495    let Some(node_path) = node_path else {
6496        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";
6497        #[cfg(target_arch = "wasm32")]
6498        web_sys::console::warn_1(&message.into());
6499        #[cfg(not(target_arch = "wasm32"))]
6500        eprintln!("WARNING: {message}");
6501        return numeric_literal_at_source_range(ast, source_range).map(Some);
6502    };
6503    let find = FindSketchVarInitialByNodePath {
6504        target: node_path,
6505        sketch_var_found: Cell::new(false),
6506        initial_literal: Cell::new(None),
6507    };
6508    let node = crate::walk::Node::from(ast);
6509    node.visit(&find).ok()?;
6510    if !find.sketch_var_found.get() {
6511        return None;
6512    }
6513    Some(find.initial_literal.into_inner())
6514}
6515
6516fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6517    match suffix {
6518        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6519        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6520        NumericSuffix::M => Some(UnitLength::Meters),
6521        NumericSuffix::Inch => Some(UnitLength::Inches),
6522        NumericSuffix::Ft => Some(UnitLength::Feet),
6523        NumericSuffix::Yd => Some(UnitLength::Yards),
6524        _ => None,
6525    }
6526}
6527
6528fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6529    match suffix_length_unit(number.units) {
6530        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6531        None => number.value,
6532    }
6533}
6534
6535fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6536    match suffix_length_unit(literal.suffix) {
6537        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6538        None => literal.value,
6539    }
6540}
6541
6542fn var_solution_needs_commit(
6543    current_literal: &ast::NumericLiteral,
6544    solved_value: Number,
6545    default_length_unit: UnitLength,
6546) -> bool {
6547    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6548    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6549
6550    (current - solved).abs() > 1e-9
6551}
6552
6553fn preserve_var_solution_literal_style(
6554    current_literal: &ast::NumericLiteral,
6555    solved_value: Number,
6556    default_length_unit: UnitLength,
6557) -> Number {
6558    if current_literal.suffix == NumericSuffix::None {
6559        return Number {
6560            value: number_value_in_default_length_units(solved_value, default_length_unit),
6561            units: NumericSuffix::None,
6562        };
6563    }
6564
6565    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6566        return solved_value;
6567    };
6568
6569    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6570    Number {
6571        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6572        units: current_literal.suffix,
6573    }
6574}
6575
6576pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6577    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6578        inner: ast::ArrayExpression {
6579            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6580            non_code_meta: Default::default(),
6581            digest: None,
6582        },
6583        start: Default::default(),
6584        end: Default::default(),
6585        module_id: Default::default(),
6586        node_path: None,
6587        outer_attrs: Default::default(),
6588        pre_comments: Default::default(),
6589        comment_start: Default::default(),
6590    })))
6591}
6592
6593pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6594    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6595        ast::ArrayExpression {
6596            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6597            digest: None,
6598            non_code_meta: Default::default(),
6599        },
6600    ))))
6601}
6602
6603fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6604    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6605        ast::ArrayExpression {
6606            elements: vec![
6607                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6608                    point.x,
6609                )?)))),
6610                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6611                    point.y,
6612                )?)))),
6613            ],
6614            non_code_meta: Default::default(),
6615            digest: None,
6616        },
6617    ))))
6618}
6619
6620fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6621    match expr {
6622        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6623            inner: ast::Literal::from(to_source_number(*number)?),
6624            start: Default::default(),
6625            end: Default::default(),
6626            module_id: Default::default(),
6627            node_path: None,
6628            outer_attrs: Default::default(),
6629            pre_comments: Default::default(),
6630            comment_start: Default::default(),
6631        }))),
6632        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6633            inner: ast::SketchVar {
6634                initial: Some(Box::new(ast::Node {
6635                    inner: to_source_number(*number)?,
6636                    start: Default::default(),
6637                    end: Default::default(),
6638                    module_id: Default::default(),
6639                    node_path: None,
6640                    outer_attrs: Default::default(),
6641                    pre_comments: Default::default(),
6642                    comment_start: Default::default(),
6643                })),
6644                digest: None,
6645            },
6646            start: Default::default(),
6647            end: Default::default(),
6648            module_id: Default::default(),
6649            node_path: None,
6650            outer_attrs: Default::default(),
6651            pre_comments: Default::default(),
6652            comment_start: Default::default(),
6653        }))),
6654        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6655    }
6656}
6657
6658fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6659    Ok(ast::NumericLiteral {
6660        value: number.value,
6661        suffix: number.units,
6662        raw: format_number_literal(number.value, number.units, None)?,
6663        digest: None,
6664    })
6665}
6666
6667pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6668    ast::Expr::Name(Box::new(ast_name(name)))
6669}
6670
6671fn ast_name(name: String) -> ast::Node<ast::Name> {
6672    ast::Node {
6673        inner: ast::Name {
6674            name: ast::Node {
6675                inner: ast::Identifier { name, digest: None },
6676                start: Default::default(),
6677                end: Default::default(),
6678                module_id: Default::default(),
6679                node_path: None,
6680                outer_attrs: Default::default(),
6681                pre_comments: Default::default(),
6682                comment_start: Default::default(),
6683            },
6684            path: Vec::new(),
6685            abs_path: false,
6686            digest: None,
6687        },
6688        start: Default::default(),
6689        end: Default::default(),
6690        module_id: Default::default(),
6691        node_path: None,
6692        outer_attrs: Default::default(),
6693        pre_comments: Default::default(),
6694        comment_start: Default::default(),
6695    }
6696}
6697
6698pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6699    ast::Name {
6700        name: ast::Node {
6701            inner: ast::Identifier {
6702                name: name.to_owned(),
6703                digest: None,
6704            },
6705            start: Default::default(),
6706            end: Default::default(),
6707            module_id: Default::default(),
6708            node_path: None,
6709            outer_attrs: Default::default(),
6710            pre_comments: Default::default(),
6711            comment_start: Default::default(),
6712        },
6713        path: Default::default(),
6714        abs_path: false,
6715        digest: None,
6716    }
6717}
6718
6719// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6720
6721/// Create an AST node for coincident([expr1, expr2, ...])
6722pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6723    let elements = exprs.into_iter().collect::<Vec<_>>();
6724    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6725
6726    // Create array [expr1, expr2, ...]
6727    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6728        elements,
6729        digest: None,
6730        non_code_meta: Default::default(),
6731    })));
6732
6733    // Create coincident([...])
6734    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6735        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6736        unlabeled: Some(array_expr),
6737        arguments: Default::default(),
6738        digest: None,
6739        non_code_meta: Default::default(),
6740    })))
6741}
6742
6743/// Create an AST node for line(start = [...], end = [...])
6744pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6745    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6746        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6747        unlabeled: None,
6748        arguments: vec![
6749            ast::LabeledArg {
6750                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6751                arg: start_ast,
6752            },
6753            ast::LabeledArg {
6754                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6755                arg: end_ast,
6756            },
6757        ],
6758        digest: None,
6759        non_code_meta: Default::default(),
6760    })))
6761}
6762
6763/// Create an AST node for arc(start = [...], end = [...], center = [...])
6764pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6765    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6766        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6767        unlabeled: None,
6768        arguments: vec![
6769            ast::LabeledArg {
6770                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6771                arg: start_ast,
6772            },
6773            ast::LabeledArg {
6774                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6775                arg: end_ast,
6776            },
6777            ast::LabeledArg {
6778                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6779                arg: center_ast,
6780            },
6781        ],
6782        digest: None,
6783        non_code_meta: Default::default(),
6784    })))
6785}
6786
6787/// Create an AST node for circle(start = [...], center = [...])
6788pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6789    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6790        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6791        unlabeled: None,
6792        arguments: vec![
6793            ast::LabeledArg {
6794                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6795                arg: start_ast,
6796            },
6797            ast::LabeledArg {
6798                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6799                arg: center_ast,
6800            },
6801        ],
6802        digest: None,
6803        non_code_meta: Default::default(),
6804    })))
6805}
6806
6807/// Create an AST node for horizontal(line)
6808pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6809    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6810        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6811        unlabeled: Some(line_expr),
6812        arguments: Default::default(),
6813        digest: None,
6814        non_code_meta: Default::default(),
6815    })))
6816}
6817
6818/// Create an AST node for vertical(line)
6819pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6820    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6821        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6822        unlabeled: Some(line_expr),
6823        arguments: Default::default(),
6824        digest: None,
6825        non_code_meta: Default::default(),
6826    })))
6827}
6828
6829/// Create a member expression like object.property (e.g., line1.end)
6830pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6831    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6832        object: object_expr,
6833        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6834            name: ast::Node::no_src(ast::Identifier {
6835                name: property.to_string(),
6836                digest: None,
6837            }),
6838            path: Vec::new(),
6839            abs_path: false,
6840            digest: None,
6841        }))),
6842        computed: false,
6843        digest: None,
6844    })))
6845}
6846
6847pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6848    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6849        object: object_expr,
6850        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6851            value: index as f64,
6852            suffix: NumericSuffix::None,
6853            raw: index.to_string(),
6854            digest: None,
6855        })))),
6856        computed: true,
6857        digest: None,
6858    })))
6859}
6860
6861/// Create an AST node for `fixed([point, [x, y]])`.
6862fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6863    // Create [x, y] array literal.
6864    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6865        position.x,
6866    )?))));
6867    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6868        position.y,
6869    )?))));
6870    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6871        elements: vec![x_literal, y_literal],
6872        digest: None,
6873        non_code_meta: Default::default(),
6874    })));
6875
6876    // Create [point, [x, y]] outer array.
6877    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6878        elements: vec![point_expr, point_array],
6879        digest: None,
6880        non_code_meta: Default::default(),
6881    })));
6882
6883    // Create fixed([...])
6884    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6885        ast::CallExpressionKw {
6886            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6887            unlabeled: Some(array_expr),
6888            arguments: Default::default(),
6889            digest: None,
6890            non_code_meta: Default::default(),
6891        },
6892    ))))
6893}
6894
6895/// Create an AST node for equalLength([line1, line2, ...])
6896pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6897    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6898        elements: line_exprs,
6899        digest: None,
6900        non_code_meta: Default::default(),
6901    })));
6902
6903    // Create equalLength([...])
6904    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6905        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6906        unlabeled: Some(array_expr),
6907        arguments: Default::default(),
6908        digest: None,
6909        non_code_meta: Default::default(),
6910    })))
6911}
6912
6913/// Create an AST node for equalRadius([seg1, seg2, ...])
6914pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6915    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6916        elements: segment_exprs,
6917        digest: None,
6918        non_code_meta: Default::default(),
6919    })));
6920
6921    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6922        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6923        unlabeled: Some(array_expr),
6924        arguments: Default::default(),
6925        digest: None,
6926        non_code_meta: Default::default(),
6927    })))
6928}
6929
6930/// Create an AST node for tangent([seg1, seg2])
6931pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6932    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6933        elements: vec![seg1_expr, seg2_expr],
6934        digest: None,
6935        non_code_meta: Default::default(),
6936    })));
6937
6938    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6939        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6940        unlabeled: Some(array_expr),
6941        arguments: Default::default(),
6942        digest: None,
6943        non_code_meta: Default::default(),
6944    })))
6945}
6946
6947/// Create an AST node for symmetric([input1, input2], axis = line)
6948pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6949    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6950        elements: input_exprs,
6951        digest: None,
6952        non_code_meta: Default::default(),
6953    })));
6954    let arguments = vec![ast::LabeledArg {
6955        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6956        arg: axis_expr,
6957    }];
6958
6959    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6960        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6961        unlabeled: Some(array_expr),
6962        arguments,
6963        digest: None,
6964        non_code_meta: Default::default(),
6965    })))
6966}
6967
6968/// Create an AST node for midpoint(segment, point = point)
6969pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6970    let arguments = vec![ast::LabeledArg {
6971        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6972        arg: point_expr,
6973    }];
6974
6975    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6976        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6977        unlabeled: Some(segment_expr),
6978        arguments,
6979        digest: None,
6980        non_code_meta: Default::default(),
6981    })))
6982}
6983
6984#[cfg(test)]
6985mod tests {
6986    use std::sync;
6987
6988    use super::*;
6989    use crate::engine::PlaneName;
6990    use crate::engine::engine_manager::EngineManager;
6991    use crate::execution::cache::SketchModeState;
6992    use crate::execution::cache::clear_mem_cache;
6993    use crate::execution::cache::read_old_memory;
6994    use crate::execution::cache::write_old_memory;
6995    use crate::front::Distance;
6996    use crate::front::Fixed;
6997    use crate::front::FixedPoint;
6998    use crate::front::Midpoint;
6999    use crate::front::Object;
7000    use crate::front::Plane;
7001    use crate::front::Sketch;
7002    use crate::front::Tangent;
7003    use crate::frontend::sketch::Vertical;
7004    use crate::pretty::NumericSuffix;
7005
7006    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7007        for object in &scene_graph.objects {
7008            if let ObjectKind::Sketch(_) = &object.kind {
7009                return Some(object);
7010            }
7011        }
7012        None
7013    }
7014
7015    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7016        for object in &scene_graph.objects {
7017            if let ObjectKind::Face(_) = &object.kind {
7018                return Some(object);
7019            }
7020        }
7021        None
7022    }
7023
7024    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7025        for object in &scene_graph.objects {
7026            if matches!(&object.kind, ObjectKind::Wall(_)) {
7027                return Some(object.id);
7028            }
7029        }
7030        None
7031    }
7032
7033    #[test]
7034    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7035        let source = "\
7036region001 = region(point = [0.1, 0.1], sketch = s)
7037extrude001 = extrude(region001, length = 5)
7038revolve001 = revolve(region001, axis = Y)
7039sweep001 = sweep(region001, path = path001)
7040loft001 = loft(region001)
7041not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7042";
7043
7044        let program = Program::parse(source).unwrap().0.unwrap();
7045
7046        assert_eq!(
7047            region_name_from_sweep_variable(&program.ast, "extrude001"),
7048            Some("region001".to_owned())
7049        );
7050        assert_eq!(
7051            region_name_from_sweep_variable(&program.ast, "revolve001"),
7052            Some("region001".to_owned())
7053        );
7054        assert_eq!(
7055            region_name_from_sweep_variable(&program.ast, "sweep001"),
7056            Some("region001".to_owned())
7057        );
7058        assert_eq!(
7059            region_name_from_sweep_variable(&program.ast, "loft001"),
7060            Some("region001".to_owned())
7061        );
7062        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7063    }
7064
7065    #[track_caller]
7066    fn expect_sketch(object: &Object) -> &Sketch {
7067        if let ObjectKind::Sketch(sketch) = &object.kind {
7068            sketch
7069        } else {
7070            panic!("Object is not a sketch: {:?}", object);
7071        }
7072    }
7073
7074    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7075        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7076        let ObjectKind::Segment {
7077            segment: Segment::Point(point),
7078        } = &point_object.kind
7079        else {
7080            panic!("Object is not a point segment: {point_object:?}");
7081        };
7082        point.position.clone()
7083    }
7084
7085    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7086        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7087        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7088    }
7089
7090    /// Build a millimeter-valued point expression for concise sketch edit test
7091    /// setup.
7092    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7093        Point2d {
7094            x: Expr::Var(Number {
7095                value: x,
7096                units: NumericSuffix::Mm,
7097            }),
7098            y: Expr::Var(Number {
7099                value: y,
7100                units: NumericSuffix::Mm,
7101            }),
7102        }
7103    }
7104
7105    /// Build a millimeter-valued numeric point for comparing solved scene graph
7106    /// positions.
7107    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7108        Point2d {
7109            x: Number {
7110                value: x,
7111                units: NumericSuffix::Mm,
7112            },
7113            y: Number {
7114                value: y,
7115                units: NumericSuffix::Mm,
7116            },
7117        }
7118    }
7119
7120    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7121        LineCtor {
7122            start: Point2d {
7123                x: Expr::Number(Number { value: start_x, units }),
7124                y: Expr::Number(Number { value: start_y, units }),
7125            },
7126            end: Point2d {
7127                x: Expr::Number(Number { value: end_x, units }),
7128                y: Expr::Number(Number { value: end_y, units }),
7129            },
7130            construction: None,
7131        }
7132    }
7133
7134    async fn create_sketch_with_single_line(
7135        frontend: &mut FrontendState,
7136        ctx: &ExecutorContext,
7137        mock_ctx: &ExecutorContext,
7138        version: Version,
7139    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7140        frontend.program = Program::empty();
7141
7142        let sketch_args = SketchCtor {
7143            on: Plane::Default(PlaneName::Xy),
7144        };
7145        let (_src_delta, _scene_delta, sketch_id) = frontend
7146            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7147            .await
7148            .unwrap();
7149
7150        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7151        let (source_delta, scene_graph_delta) = frontend
7152            .add_segment(mock_ctx, version, sketch_id, segment, None)
7153            .await
7154            .unwrap();
7155        let line_id = *scene_graph_delta
7156            .new_objects
7157            .last()
7158            .expect("Expected line object id to be created");
7159
7160        (sketch_id, line_id, source_delta, scene_graph_delta)
7161    }
7162
7163    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7164        frontend.program = program.clone();
7165        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7166        frontend.update_state_after_exec(outcome, true);
7167    }
7168
7169    #[tokio::test(flavor = "multi_thread")]
7170    async fn test_sketch_checkpoint_round_trip_restores_state() {
7171        let mut frontend = FrontendState::new();
7172        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7173        let mock_ctx = ExecutorContext::new_mock(None).await;
7174        let version = Version(0);
7175
7176        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7177            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7178
7179        let expected_source = source_delta.text.clone();
7180        let expected_scene_graph = frontend.scene_graph.clone();
7181        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7182        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7183
7184        let checkpoint_id = frontend
7185            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7186            .await
7187            .unwrap();
7188
7189        let edited_segments = vec![ExistingSegmentCtor {
7190            id: line_id,
7191            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7192        }];
7193        let (edited_source, _edited_scene) = frontend
7194            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7195            .await
7196            .unwrap();
7197        assert_ne!(edited_source.text, expected_source);
7198
7199        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7200
7201        assert_eq!(restored.source_delta.text, expected_source);
7202        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7203        assert!(restored.scene_graph_delta.invalidates_ids);
7204        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7205        assert_eq!(frontend.scene_graph, expected_scene_graph);
7206        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7207
7208        ctx.close().await;
7209    }
7210
7211    #[tokio::test(flavor = "multi_thread")]
7212    async fn test_sketch_checkpoints_prune_oldest_entries() {
7213        let mut frontend = FrontendState::new();
7214        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7215        let mock_ctx = ExecutorContext::new_mock(None).await;
7216        let version = Version(0);
7217
7218        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7219            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7220
7221        let mut checkpoint_ids = Vec::new();
7222        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7223            checkpoint_ids.push(
7224                frontend
7225                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7226                    .await
7227                    .unwrap(),
7228            );
7229        }
7230
7231        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7232        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7233
7234        let oldest_retained = checkpoint_ids[3];
7235        assert_eq!(
7236            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7237            Some(oldest_retained)
7238        );
7239
7240        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7241        assert!(evicted_restore.is_err());
7242        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7243
7244        frontend
7245            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7246            .await
7247            .unwrap();
7248
7249        ctx.close().await;
7250    }
7251
7252    #[tokio::test(flavor = "multi_thread")]
7253    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7254        let mut frontend = FrontendState::new();
7255        let missing_checkpoint = SketchCheckpointId::new(999);
7256
7257        let err = frontend
7258            .restore_sketch_checkpoint(missing_checkpoint)
7259            .await
7260            .expect_err("Expected restore to fail for missing checkpoint");
7261
7262        assert!(err.msg.contains("Sketch checkpoint not found"));
7263    }
7264
7265    #[tokio::test(flavor = "multi_thread")]
7266    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7267        let mut frontend = FrontendState::new();
7268        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7269        let mock_ctx = ExecutorContext::new_mock(None).await;
7270        let version = Version(0);
7271
7272        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7273            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7274
7275        let checkpoint_a = frontend
7276            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7277            .await
7278            .unwrap();
7279        let checkpoint_b = frontend
7280            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7281            .await
7282            .unwrap();
7283        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7284
7285        frontend.clear_sketch_checkpoints();
7286        assert!(frontend.sketch_checkpoints.is_empty());
7287        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7288        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7289
7290        ctx.close().await;
7291    }
7292
7293    #[tokio::test(flavor = "multi_thread")]
7294    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7295        let mut frontend = FrontendState::new();
7296        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7297        let mock_ctx = ExecutorContext::new_mock(None).await;
7298        let version = Version(0);
7299
7300        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7301            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7302        let old_source = source_delta.text.clone();
7303        let old_checkpoint = frontend
7304            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7305            .await
7306            .unwrap();
7307        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7308
7309        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7310            .unwrap()
7311            .0
7312            .unwrap();
7313
7314        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7315        let SetProgramOutcome::Success {
7316            checkpoint_id: Some(new_checkpoint),
7317            ..
7318        } = result
7319        else {
7320            panic!("Expected Success with a fresh checkpoint baseline");
7321        };
7322
7323        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7324
7325        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7326        assert_eq!(old_restore.source_delta.text, old_source);
7327
7328        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7329        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7330
7331        ctx.close().await;
7332    }
7333
7334    #[tokio::test(flavor = "multi_thread")]
7335    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7336        let mut frontend = FrontendState::new();
7337        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7338        let mock_ctx = ExecutorContext::new_mock(None).await;
7339        let version = Version(0);
7340
7341        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7342            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7343        let old_checkpoint = frontend
7344            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7345            .await
7346            .unwrap();
7347        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7348
7349        let failing_program = Program::parse(
7350            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7351        )
7352        .unwrap()
7353        .0
7354        .unwrap();
7355
7356        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7357        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7358        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7359        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7360
7361        ctx.close().await;
7362    }
7363
7364    #[tokio::test(flavor = "multi_thread")]
7365    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7366        let mut frontend = FrontendState::new();
7367        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7368
7369        let program = Program::parse(
7370            "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",
7371        )
7372        .unwrap()
7373        .0
7374        .unwrap();
7375        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7376        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7377            panic!("Expected successful baseline program execution");
7378        };
7379
7380        clear_mem_cache().await;
7381        assert!(read_old_memory().await.is_none());
7382
7383        let checkpoint_without_mock_memory = frontend
7384            .create_sketch_checkpoint((*exec_outcome).clone())
7385            .await
7386            .unwrap();
7387
7388        write_old_memory(SketchModeState::new_for_tests()).await;
7389        assert!(read_old_memory().await.is_some());
7390
7391        let checkpoint_with_mock_memory = frontend
7392            .create_sketch_checkpoint((*exec_outcome).clone())
7393            .await
7394            .unwrap();
7395
7396        clear_mem_cache().await;
7397        assert!(read_old_memory().await.is_none());
7398
7399        frontend
7400            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7401            .await
7402            .unwrap();
7403        assert!(read_old_memory().await.is_some());
7404
7405        frontend
7406            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7407            .await
7408            .unwrap();
7409        assert!(read_old_memory().await.is_none());
7410
7411        ctx.close().await;
7412    }
7413
7414    #[tokio::test(flavor = "multi_thread")]
7415    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7416        let source = "\
7417sketch(on = XY) {
7418  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7419}
7420
7421bad = missing_name
7422";
7423        let program = Program::parse(source).unwrap().0.unwrap();
7424
7425        let mut frontend = FrontendState::new();
7426
7427        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7428        let mock_ctx = ExecutorContext::new_mock(None).await;
7429        let version = Version(0);
7430        let project_id = ProjectId(0);
7431        let file_id = FileId(0);
7432
7433        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7434            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7435        };
7436
7437        let sketch_id = frontend
7438            .scene_graph
7439            .objects
7440            .iter()
7441            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7442            .expect("Expected sketch object from errored hack_set_program");
7443
7444        frontend
7445            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7446            .await
7447            .unwrap();
7448
7449        ctx.close().await;
7450        mock_ctx.close().await;
7451    }
7452
7453    #[tokio::test(flavor = "multi_thread")]
7454    async fn test_new_sketch_add_point_edit_point() {
7455        let program = Program::empty();
7456
7457        let mut frontend = FrontendState::new();
7458        frontend.program = program;
7459
7460        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7461        let mock_ctx = ExecutorContext::new_mock(None).await;
7462        let version = Version(0);
7463
7464        let sketch_args = SketchCtor {
7465            on: Plane::Default(PlaneName::Xy),
7466        };
7467        let (_src_delta, scene_delta, sketch_id) = frontend
7468            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7469            .await
7470            .unwrap();
7471        assert_eq!(sketch_id, ObjectId(1));
7472        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7473        let sketch_object = &scene_delta.new_graph.objects[1];
7474        assert_eq!(sketch_object.id, ObjectId(1));
7475        assert_eq!(
7476            sketch_object.kind,
7477            ObjectKind::Sketch(Sketch {
7478                args: SketchCtor {
7479                    on: Plane::Default(PlaneName::Xy)
7480                },
7481                plane: ObjectId(0),
7482                segments: vec![],
7483                constraints: vec![],
7484            })
7485        );
7486        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7487
7488        let point_ctor = PointCtor {
7489            position: Point2d {
7490                x: Expr::Number(Number {
7491                    value: 1.0,
7492                    units: NumericSuffix::Inch,
7493                }),
7494                y: Expr::Number(Number {
7495                    value: 2.0,
7496                    units: NumericSuffix::Inch,
7497                }),
7498            },
7499        };
7500        let segment = SegmentCtor::Point(point_ctor);
7501        let (src_delta, scene_delta) = frontend
7502            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7503            .await
7504            .unwrap();
7505        assert_eq!(
7506            src_delta.text.as_str(),
7507            "sketch001 = sketch(on = XY) {
7508  point(at = [1in, 2in])
7509}
7510"
7511        );
7512        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7513        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7514        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7515            assert_eq!(scene_object.id.0, i);
7516        }
7517
7518        let point_id = *scene_delta.new_objects.last().unwrap();
7519
7520        let point_ctor = PointCtor {
7521            position: Point2d {
7522                x: Expr::Number(Number {
7523                    value: 3.0,
7524                    units: NumericSuffix::Inch,
7525                }),
7526                y: Expr::Number(Number {
7527                    value: 4.0,
7528                    units: NumericSuffix::Inch,
7529                }),
7530            },
7531        };
7532        let segments = vec![ExistingSegmentCtor {
7533            id: point_id,
7534            ctor: SegmentCtor::Point(point_ctor),
7535        }];
7536        let (src_delta, scene_delta) = frontend
7537            .edit_segments(&mock_ctx, version, sketch_id, segments)
7538            .await
7539            .unwrap();
7540        assert_eq!(
7541            src_delta.text.as_str(),
7542            "sketch001 = sketch(on = XY) {
7543  point(at = [3in, 4in])
7544}
7545"
7546        );
7547        assert_eq!(scene_delta.new_objects, vec![]);
7548        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7549
7550        ctx.close().await;
7551        mock_ctx.close().await;
7552    }
7553
7554    #[tokio::test(flavor = "multi_thread")]
7555    async fn test_new_sketch_add_line_edit_line() {
7556        let program = Program::empty();
7557
7558        let mut frontend = FrontendState::new();
7559        frontend.program = program;
7560
7561        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7562        let mock_ctx = ExecutorContext::new_mock(None).await;
7563        let version = Version(0);
7564
7565        let sketch_args = SketchCtor {
7566            on: Plane::Default(PlaneName::Xy),
7567        };
7568        let (_src_delta, scene_delta, sketch_id) = frontend
7569            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7570            .await
7571            .unwrap();
7572        assert_eq!(sketch_id, ObjectId(1));
7573        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7574        let sketch_object = &scene_delta.new_graph.objects[1];
7575        assert_eq!(sketch_object.id, ObjectId(1));
7576        assert_eq!(
7577            sketch_object.kind,
7578            ObjectKind::Sketch(Sketch {
7579                args: SketchCtor {
7580                    on: Plane::Default(PlaneName::Xy)
7581                },
7582                plane: ObjectId(0),
7583                segments: vec![],
7584                constraints: vec![],
7585            })
7586        );
7587        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7588
7589        let line_ctor = LineCtor {
7590            start: Point2d {
7591                x: Expr::Number(Number {
7592                    value: 0.0,
7593                    units: NumericSuffix::Mm,
7594                }),
7595                y: Expr::Number(Number {
7596                    value: 0.0,
7597                    units: NumericSuffix::Mm,
7598                }),
7599            },
7600            end: Point2d {
7601                x: Expr::Number(Number {
7602                    value: 10.0,
7603                    units: NumericSuffix::Mm,
7604                }),
7605                y: Expr::Number(Number {
7606                    value: 10.0,
7607                    units: NumericSuffix::Mm,
7608                }),
7609            },
7610            construction: None,
7611        };
7612        let segment = SegmentCtor::Line(line_ctor);
7613        let (src_delta, scene_delta) = frontend
7614            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7615            .await
7616            .unwrap();
7617        assert_eq!(
7618            src_delta.text.as_str(),
7619            "sketch001 = sketch(on = XY) {
7620  line(start = [0mm, 0mm], end = [10mm, 10mm])
7621}
7622"
7623        );
7624        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7625        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7626        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7627            assert_eq!(scene_object.id.0, i);
7628        }
7629
7630        // The new objects are the end points and then the line.
7631        let line = *scene_delta.new_objects.last().unwrap();
7632
7633        let line_ctor = LineCtor {
7634            start: Point2d {
7635                x: Expr::Number(Number {
7636                    value: 1.0,
7637                    units: NumericSuffix::Mm,
7638                }),
7639                y: Expr::Number(Number {
7640                    value: 2.0,
7641                    units: NumericSuffix::Mm,
7642                }),
7643            },
7644            end: Point2d {
7645                x: Expr::Number(Number {
7646                    value: 13.0,
7647                    units: NumericSuffix::Mm,
7648                }),
7649                y: Expr::Number(Number {
7650                    value: 14.0,
7651                    units: NumericSuffix::Mm,
7652                }),
7653            },
7654            construction: None,
7655        };
7656        let segments = vec![ExistingSegmentCtor {
7657            id: line,
7658            ctor: SegmentCtor::Line(line_ctor),
7659        }];
7660        let (src_delta, scene_delta) = frontend
7661            .edit_segments(&mock_ctx, version, sketch_id, segments)
7662            .await
7663            .unwrap();
7664        assert_eq!(
7665            src_delta.text.as_str(),
7666            "sketch001 = sketch(on = XY) {
7667  line(start = [1mm, 2mm], end = [13mm, 14mm])
7668}
7669"
7670        );
7671        assert_eq!(scene_delta.new_objects, vec![]);
7672        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7673
7674        ctx.close().await;
7675        mock_ctx.close().await;
7676    }
7677
7678    #[tokio::test(flavor = "multi_thread")]
7679    async fn test_new_sketch_add_arc_edit_arc() {
7680        let program = Program::empty();
7681
7682        let mut frontend = FrontendState::new();
7683        frontend.program = program;
7684
7685        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7686        let mock_ctx = ExecutorContext::new_mock(None).await;
7687        let version = Version(0);
7688
7689        let sketch_args = SketchCtor {
7690            on: Plane::Default(PlaneName::Xy),
7691        };
7692        let (_src_delta, scene_delta, sketch_id) = frontend
7693            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7694            .await
7695            .unwrap();
7696        assert_eq!(sketch_id, ObjectId(1));
7697        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7698        let sketch_object = &scene_delta.new_graph.objects[1];
7699        assert_eq!(sketch_object.id, ObjectId(1));
7700        assert_eq!(
7701            sketch_object.kind,
7702            ObjectKind::Sketch(Sketch {
7703                args: SketchCtor {
7704                    on: Plane::Default(PlaneName::Xy),
7705                },
7706                plane: ObjectId(0),
7707                segments: vec![],
7708                constraints: vec![],
7709            })
7710        );
7711        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7712
7713        let arc_ctor = ArcCtor {
7714            start: Point2d {
7715                x: Expr::Var(Number {
7716                    value: 0.0,
7717                    units: NumericSuffix::Mm,
7718                }),
7719                y: Expr::Var(Number {
7720                    value: 0.0,
7721                    units: NumericSuffix::Mm,
7722                }),
7723            },
7724            end: Point2d {
7725                x: Expr::Var(Number {
7726                    value: 10.0,
7727                    units: NumericSuffix::Mm,
7728                }),
7729                y: Expr::Var(Number {
7730                    value: 10.0,
7731                    units: NumericSuffix::Mm,
7732                }),
7733            },
7734            center: Point2d {
7735                x: Expr::Var(Number {
7736                    value: 10.0,
7737                    units: NumericSuffix::Mm,
7738                }),
7739                y: Expr::Var(Number {
7740                    value: 0.0,
7741                    units: NumericSuffix::Mm,
7742                }),
7743            },
7744            construction: None,
7745        };
7746        let segment = SegmentCtor::Arc(arc_ctor);
7747        let (src_delta, scene_delta) = frontend
7748            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7749            .await
7750            .unwrap();
7751        assert_eq!(
7752            src_delta.text.as_str(),
7753            "sketch001 = sketch(on = XY) {
7754  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7755}
7756"
7757        );
7758        assert_eq!(
7759            scene_delta.new_objects,
7760            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7761        );
7762        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7763            assert_eq!(scene_object.id.0, i);
7764        }
7765        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7766
7767        // The new objects are the end points, the center, and then the arc.
7768        let arc = *scene_delta.new_objects.last().unwrap();
7769
7770        let arc_ctor = ArcCtor {
7771            start: Point2d {
7772                x: Expr::Var(Number {
7773                    value: 1.0,
7774                    units: NumericSuffix::Mm,
7775                }),
7776                y: Expr::Var(Number {
7777                    value: 2.0,
7778                    units: NumericSuffix::Mm,
7779                }),
7780            },
7781            end: Point2d {
7782                x: Expr::Var(Number {
7783                    value: 13.0,
7784                    units: NumericSuffix::Mm,
7785                }),
7786                y: Expr::Var(Number {
7787                    value: 14.0,
7788                    units: NumericSuffix::Mm,
7789                }),
7790            },
7791            center: Point2d {
7792                x: Expr::Var(Number {
7793                    value: 13.0,
7794                    units: NumericSuffix::Mm,
7795                }),
7796                y: Expr::Var(Number {
7797                    value: 2.0,
7798                    units: NumericSuffix::Mm,
7799                }),
7800            },
7801            construction: None,
7802        };
7803        let segments = vec![ExistingSegmentCtor {
7804            id: arc,
7805            ctor: SegmentCtor::Arc(arc_ctor),
7806        }];
7807        let (src_delta, scene_delta) = frontend
7808            .edit_segments(&mock_ctx, version, sketch_id, segments)
7809            .await
7810            .unwrap();
7811        assert_eq!(
7812            src_delta.text.as_str(),
7813            "sketch001 = sketch(on = XY) {
7814  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7815}
7816"
7817        );
7818        assert_eq!(scene_delta.new_objects, vec![]);
7819        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7820
7821        ctx.close().await;
7822        mock_ctx.close().await;
7823    }
7824
7825    #[tokio::test(flavor = "multi_thread")]
7826    async fn test_new_sketch_add_circle_edit_circle() {
7827        let program = Program::empty();
7828
7829        let mut frontend = FrontendState::new();
7830        frontend.program = program;
7831
7832        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7833        let mock_ctx = ExecutorContext::new_mock(None).await;
7834        let version = Version(0);
7835
7836        let sketch_args = SketchCtor {
7837            on: Plane::Default(PlaneName::Xy),
7838        };
7839        let (_src_delta, _scene_delta, sketch_id) = frontend
7840            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7841            .await
7842            .unwrap();
7843
7844        // Add a circle segment.
7845        let circle_ctor = CircleCtor {
7846            start: Point2d {
7847                x: Expr::Var(Number {
7848                    value: 5.0,
7849                    units: NumericSuffix::Mm,
7850                }),
7851                y: Expr::Var(Number {
7852                    value: 0.0,
7853                    units: NumericSuffix::Mm,
7854                }),
7855            },
7856            center: Point2d {
7857                x: Expr::Var(Number {
7858                    value: 0.0,
7859                    units: NumericSuffix::Mm,
7860                }),
7861                y: Expr::Var(Number {
7862                    value: 0.0,
7863                    units: NumericSuffix::Mm,
7864                }),
7865            },
7866            construction: None,
7867        };
7868        let segment = SegmentCtor::Circle(circle_ctor);
7869        let (src_delta, scene_delta) = frontend
7870            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7871            .await
7872            .unwrap();
7873        assert_eq!(
7874            src_delta.text.as_str(),
7875            "sketch001 = sketch(on = XY) {
7876  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7877}
7878"
7879        );
7880        // The new objects are start, center, and then the circle segment.
7881        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7882        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7883
7884        let circle = *scene_delta.new_objects.last().unwrap();
7885
7886        // Edit the circle segment.
7887        let circle_ctor = CircleCtor {
7888            start: Point2d {
7889                x: Expr::Var(Number {
7890                    value: 10.0,
7891                    units: NumericSuffix::Mm,
7892                }),
7893                y: Expr::Var(Number {
7894                    value: 0.0,
7895                    units: NumericSuffix::Mm,
7896                }),
7897            },
7898            center: Point2d {
7899                x: Expr::Var(Number {
7900                    value: 3.0,
7901                    units: NumericSuffix::Mm,
7902                }),
7903                y: Expr::Var(Number {
7904                    value: 4.0,
7905                    units: NumericSuffix::Mm,
7906                }),
7907            },
7908            construction: None,
7909        };
7910        let segments = vec![ExistingSegmentCtor {
7911            id: circle,
7912            ctor: SegmentCtor::Circle(circle_ctor),
7913        }];
7914        let (src_delta, scene_delta) = frontend
7915            .edit_segments(&mock_ctx, version, sketch_id, segments)
7916            .await
7917            .unwrap();
7918        assert_eq!(
7919            src_delta.text.as_str(),
7920            "sketch001 = sketch(on = XY) {
7921  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7922}
7923"
7924        );
7925        assert_eq!(scene_delta.new_objects, vec![]);
7926        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7927
7928        ctx.close().await;
7929        mock_ctx.close().await;
7930    }
7931
7932    #[tokio::test(flavor = "multi_thread")]
7933    async fn test_delete_circle() {
7934        let initial_source = "sketch001 = sketch(on = XY) {
7935  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7936}
7937";
7938
7939        let program = Program::parse(initial_source).unwrap().0.unwrap();
7940        let mut frontend = FrontendState::new();
7941
7942        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7943        let mock_ctx = ExecutorContext::new_mock(None).await;
7944        let version = Version(0);
7945
7946        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7947        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7948        let sketch_id = sketch_object.id;
7949        let sketch = expect_sketch(sketch_object);
7950
7951        // The sketch should have 3 segments: start point, center point, and the circle.
7952        assert_eq!(sketch.segments.len(), 3);
7953        let circle_id = sketch.segments[2];
7954
7955        // Delete the circle.
7956        let (src_delta, scene_delta) = frontend
7957            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7958            .await
7959            .unwrap();
7960        assert_eq!(
7961            src_delta.text.as_str(),
7962            "sketch001 = sketch(on = XY) {
7963}
7964"
7965        );
7966        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7967        let new_sketch = expect_sketch(new_sketch_object);
7968        assert_eq!(new_sketch.segments.len(), 0);
7969
7970        ctx.close().await;
7971        mock_ctx.close().await;
7972    }
7973
7974    #[tokio::test(flavor = "multi_thread")]
7975    async fn test_edit_circle_via_point() {
7976        let initial_source = "sketch001 = sketch(on = XY) {
7977  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7978}
7979";
7980
7981        let program = Program::parse(initial_source).unwrap().0.unwrap();
7982        let mut frontend = FrontendState::new();
7983
7984        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7985        let mock_ctx = ExecutorContext::new_mock(None).await;
7986        let version = Version(0);
7987
7988        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7989        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7990        let sketch_id = sketch_object.id;
7991        let sketch = expect_sketch(sketch_object);
7992
7993        // Find the circle segment and its start point.
7994        let circle_id = sketch
7995            .segments
7996            .iter()
7997            .copied()
7998            .find(|seg_id| {
7999                matches!(
8000                    &frontend.scene_graph.objects[seg_id.0].kind,
8001                    ObjectKind::Segment {
8002                        segment: Segment::Circle(_)
8003                    }
8004                )
8005            })
8006            .expect("Expected a circle segment in sketch");
8007        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8008        let ObjectKind::Segment {
8009            segment: Segment::Circle(circle),
8010        } = &circle_object.kind
8011        else {
8012            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8013        };
8014        let start_point_id = circle.start;
8015
8016        // Edit the start point via SegmentCtor::Point.
8017        let segments = vec![ExistingSegmentCtor {
8018            id: start_point_id,
8019            ctor: SegmentCtor::Point(PointCtor {
8020                position: Point2d {
8021                    x: Expr::Var(Number {
8022                        value: 7.0,
8023                        units: NumericSuffix::Mm,
8024                    }),
8025                    y: Expr::Var(Number {
8026                        value: 1.0,
8027                        units: NumericSuffix::Mm,
8028                    }),
8029                },
8030            }),
8031        }];
8032        let (src_delta, _scene_delta) = frontend
8033            .edit_segments(&mock_ctx, version, sketch_id, segments)
8034            .await
8035            .unwrap();
8036        assert_eq!(
8037            src_delta.text.as_str(),
8038            "sketch001 = sketch(on = XY) {
8039  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8040}
8041"
8042        );
8043
8044        ctx.close().await;
8045        mock_ctx.close().await;
8046    }
8047
8048    #[tokio::test(flavor = "multi_thread")]
8049    async fn test_add_line_when_sketch_block_uses_variable() {
8050        let initial_source = "s = sketch(on = XY) {}
8051";
8052
8053        let program = Program::parse(initial_source).unwrap().0.unwrap();
8054
8055        let mut frontend = FrontendState::new();
8056
8057        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8058        let mock_ctx = ExecutorContext::new_mock(None).await;
8059        let version = Version(0);
8060
8061        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8062        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8063        let sketch_id = sketch_object.id;
8064
8065        let line_ctor = LineCtor {
8066            start: Point2d {
8067                x: Expr::Number(Number {
8068                    value: 0.0,
8069                    units: NumericSuffix::Mm,
8070                }),
8071                y: Expr::Number(Number {
8072                    value: 0.0,
8073                    units: NumericSuffix::Mm,
8074                }),
8075            },
8076            end: Point2d {
8077                x: Expr::Number(Number {
8078                    value: 10.0,
8079                    units: NumericSuffix::Mm,
8080                }),
8081                y: Expr::Number(Number {
8082                    value: 10.0,
8083                    units: NumericSuffix::Mm,
8084                }),
8085            },
8086            construction: None,
8087        };
8088        let segment = SegmentCtor::Line(line_ctor);
8089        let (src_delta, scene_delta) = frontend
8090            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091            .await
8092            .unwrap();
8093        assert_eq!(
8094            src_delta.text.as_str(),
8095            "s = sketch(on = XY) {
8096  line(start = [0mm, 0mm], end = [10mm, 10mm])
8097}
8098"
8099        );
8100        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8101        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8102
8103        ctx.close().await;
8104        mock_ctx.close().await;
8105    }
8106
8107    #[tokio::test(flavor = "multi_thread")]
8108    async fn test_new_sketch_add_line_delete_sketch() {
8109        let program = Program::empty();
8110
8111        let mut frontend = FrontendState::new();
8112        frontend.program = program;
8113
8114        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8115        let mock_ctx = ExecutorContext::new_mock(None).await;
8116        let version = Version(0);
8117
8118        let sketch_args = SketchCtor {
8119            on: Plane::Default(PlaneName::Xy),
8120        };
8121        let (_src_delta, scene_delta, sketch_id) = frontend
8122            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8123            .await
8124            .unwrap();
8125        assert_eq!(sketch_id, ObjectId(1));
8126        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8127        let sketch_object = &scene_delta.new_graph.objects[1];
8128        assert_eq!(sketch_object.id, ObjectId(1));
8129        assert_eq!(
8130            sketch_object.kind,
8131            ObjectKind::Sketch(Sketch {
8132                args: SketchCtor {
8133                    on: Plane::Default(PlaneName::Xy)
8134                },
8135                plane: ObjectId(0),
8136                segments: vec![],
8137                constraints: vec![],
8138            })
8139        );
8140        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8141
8142        let line_ctor = LineCtor {
8143            start: Point2d {
8144                x: Expr::Number(Number {
8145                    value: 0.0,
8146                    units: NumericSuffix::Mm,
8147                }),
8148                y: Expr::Number(Number {
8149                    value: 0.0,
8150                    units: NumericSuffix::Mm,
8151                }),
8152            },
8153            end: Point2d {
8154                x: Expr::Number(Number {
8155                    value: 10.0,
8156                    units: NumericSuffix::Mm,
8157                }),
8158                y: Expr::Number(Number {
8159                    value: 10.0,
8160                    units: NumericSuffix::Mm,
8161                }),
8162            },
8163            construction: None,
8164        };
8165        let segment = SegmentCtor::Line(line_ctor);
8166        let (src_delta, scene_delta) = frontend
8167            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8168            .await
8169            .unwrap();
8170        assert_eq!(
8171            src_delta.text.as_str(),
8172            "sketch001 = sketch(on = XY) {
8173  line(start = [0mm, 0mm], end = [10mm, 10mm])
8174}
8175"
8176        );
8177        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8178
8179        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8180        assert_eq!(src_delta.text.as_str(), "");
8181        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8182
8183        ctx.close().await;
8184        mock_ctx.close().await;
8185    }
8186
8187    #[tokio::test(flavor = "multi_thread")]
8188    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8189        let initial_source = "s = sketch(on = XY) {}
8190";
8191
8192        let program = Program::parse(initial_source).unwrap().0.unwrap();
8193
8194        let mut frontend = FrontendState::new();
8195
8196        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8197        let version = Version(0);
8198
8199        frontend.hack_set_program(&ctx, program).await.unwrap();
8200        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8201        let sketch_id = sketch_object.id;
8202
8203        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8204        assert_eq!(src_delta.text.as_str(), "");
8205        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8206
8207        ctx.close().await;
8208    }
8209
8210    #[tokio::test(flavor = "multi_thread")]
8211    async fn test_delete_sketch_after_comment() {
8212        let initial_source = "sketch001 = sketch(on = XZ) {
8213}
8214";
8215
8216        let program = Program::parse(initial_source).unwrap().0.unwrap();
8217        let mut frontend = FrontendState::new();
8218
8219        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8220        let version = Version(0);
8221
8222        frontend.hack_set_program(&ctx, program).await.unwrap();
8223        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8224        let sketch_id = sketch_object.id;
8225        let original_source = sketch_object.source.clone();
8226
8227        let commented_source = "// test 1
8228sketch001 = sketch(on = XZ) {
8229}
8230";
8231        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8232        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8233
8234        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8235        assert_eq!(cached_sketch_object.source, original_source);
8236
8237        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8238        assert!(
8239            !src_delta.text.contains("sketch001"),
8240            "sketch was not deleted: {}",
8241            src_delta.text
8242        );
8243        // The leading line comment must survive deletion.
8244        assert_eq!(src_delta.text.as_str(), "// test 1\n");
8245        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8246
8247        ctx.close().await;
8248    }
8249
8250    #[tokio::test(flavor = "multi_thread")]
8251    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8252        let initial_source = "sketch001 = sketch(on = XZ) {
8253}
8254foo = 1
8255";
8256
8257        let program = Program::parse(initial_source).unwrap().0.unwrap();
8258        let mut frontend = FrontendState::new();
8259
8260        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8261        let version = Version(0);
8262
8263        frontend.hack_set_program(&ctx, program).await.unwrap();
8264        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8265        let sketch_id = sketch_object.id;
8266
8267        let commented_source = "// keep me
8268sketch001 = sketch(on = XZ) {
8269}
8270foo = 1
8271";
8272        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8273        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8274
8275        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8276        // The leading comment should remain, now attached to the following body item.
8277        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8278
8279        ctx.close().await;
8280    }
8281
8282    #[tokio::test(flavor = "multi_thread")]
8283    async fn test_delete_segment_preserves_pre_comment() {
8284        let initial_source = "\
8285sketch(on = XY) {
8286  point(at = [var 1, var 2])
8287  // describe the middle point
8288  point(at = [var 3, var 4])
8289  point(at = [var 5, var 6])
8290}
8291";
8292
8293        let program = Program::parse(initial_source).unwrap().0.unwrap();
8294        let mut frontend = FrontendState::new();
8295
8296        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8297        let mock_ctx = ExecutorContext::new_mock(None).await;
8298        let version = Version(0);
8299
8300        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8301        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8302        let sketch_id = sketch_object.id;
8303        let sketch = expect_sketch(sketch_object);
8304
8305        let middle_point_id = *sketch.segments.get(1).unwrap();
8306
8307        let (src_delta, _scene_delta) = frontend
8308            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8309            .await
8310            .unwrap();
8311        // The line comment on the line above the deleted point must be preserved.
8312        // It is reattached to the next surviving body item.
8313        assert_eq!(
8314            src_delta.text.as_str(),
8315            "\
8316sketch(on = XY) {
8317  point(at = [var 1, var 2])
8318  // describe the middle point
8319  point(at = [var 5, var 6])
8320}
8321"
8322        );
8323
8324        ctx.close().await;
8325        mock_ctx.close().await;
8326    }
8327
8328    #[tokio::test(flavor = "multi_thread")]
8329    async fn test_delete_last_segment_preserves_pre_comment() {
8330        let initial_source = "\
8331sketch(on = XY) {
8332  point(at = [var 1, var 2])
8333  // describe the trailing point
8334  point(at = [var 3, var 4])
8335}
8336";
8337
8338        let program = Program::parse(initial_source).unwrap().0.unwrap();
8339        let mut frontend = FrontendState::new();
8340
8341        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8342        let mock_ctx = ExecutorContext::new_mock(None).await;
8343        let version = Version(0);
8344
8345        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8346        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8347        let sketch_id = sketch_object.id;
8348        let sketch = expect_sketch(sketch_object);
8349
8350        let last_point_id = *sketch.segments.last().unwrap();
8351
8352        let (src_delta, _scene_delta) = frontend
8353            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8354            .await
8355            .unwrap();
8356        // No following item to attach to; the comment is kept inside the sketch
8357        // block as trailing non-code metadata so the user does not lose it.
8358        assert_eq!(
8359            src_delta.text.as_str(),
8360            "\
8361sketch(on = XY) {
8362  point(at = [var 1, var 2])
8363  // describe the trailing point
8364}
8365"
8366        );
8367
8368        ctx.close().await;
8369        mock_ctx.close().await;
8370    }
8371
8372    #[tokio::test(flavor = "multi_thread")]
8373    async fn test_delete_segment_drops_inline_trailing_comment() {
8374        let initial_source = "\
8375sketch(on = XY) {
8376  point(at = [var 1, var 2])
8377  point(at = [var 3, var 4]) // same-line note that gets dropped
8378  point(at = [var 5, var 6])
8379}
8380";
8381
8382        let program = Program::parse(initial_source).unwrap().0.unwrap();
8383        let mut frontend = FrontendState::new();
8384
8385        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8386        let mock_ctx = ExecutorContext::new_mock(None).await;
8387        let version = Version(0);
8388
8389        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8390        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8391        let sketch_id = sketch_object.id;
8392        let sketch = expect_sketch(sketch_object);
8393
8394        let middle_point_id = *sketch.segments.get(1).unwrap();
8395
8396        let (src_delta, _scene_delta) = frontend
8397            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8398            .await
8399            .unwrap();
8400        // The same-line trailing comment is removed along with the deleted code.
8401        assert!(
8402            !src_delta.text.contains("same-line note"),
8403            "inline comment should have been removed: {}",
8404            src_delta.text
8405        );
8406
8407        ctx.close().await;
8408        mock_ctx.close().await;
8409    }
8410
8411    #[tokio::test(flavor = "multi_thread")]
8412    async fn test_delete_segments_preserves_block_comments_across_positions() {
8413        // One test exercising several `delete_body_item_preserving_pre_comments`
8414        // branches at once with `/* ... */` block comments:
8415        //   - first point: leading block comment must migrate to the next item.
8416        //   - first point: same-line trailing block comment must be dropped.
8417        //   - middle point: leading block comment must stay attached after migration.
8418        //   - last point: leading block comment, with no surviving next item,
8419        //     must be converted into a trailing NonCodeNode.
8420        let initial_source = "\
8421sketch(on = XY) {
8422  /* above first - moves to middle */
8423  point(at = [var 1, var 2]) /* same-line on first - dropped */
8424  /* above middle - stays */
8425  point(at = [var 3, var 4])
8426  /* above last - moves to trailing meta */
8427  point(at = [var 5, var 6])
8428}
8429";
8430
8431        let program = Program::parse(initial_source).unwrap().0.unwrap();
8432        let mut frontend = FrontendState::new();
8433
8434        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8435        let mock_ctx = ExecutorContext::new_mock(None).await;
8436        let version = Version(0);
8437
8438        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8439        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8440        let sketch_id = sketch_object.id;
8441        let sketch = expect_sketch(sketch_object);
8442
8443        let first_point_id = *sketch.segments.first().unwrap();
8444        let last_point_id = *sketch.segments.last().unwrap();
8445
8446        let (src_delta, _scene_delta) = frontend
8447            .delete_objects(
8448                &mock_ctx,
8449                version,
8450                sketch_id,
8451                Vec::new(),
8452                vec![first_point_id, last_point_id],
8453            )
8454            .await
8455            .unwrap();
8456        assert_eq!(
8457            src_delta.text.as_str(),
8458            "\
8459sketch(on = XY) {
8460  /* above first - moves to middle */
8461  /* above middle - stays */
8462  point(at = [var 3, var 4])
8463  /* above last - moves to trailing meta */
8464}
8465"
8466        );
8467
8468        ctx.close().await;
8469        mock_ctx.close().await;
8470    }
8471
8472    #[tokio::test(flavor = "multi_thread")]
8473    async fn test_edit_line_when_editing_its_start_point() {
8474        let initial_source = "\
8475sketch(on = XY) {
8476  line(start = [var 1, var 2], end = [var 3, var 4])
8477}
8478";
8479
8480        let program = Program::parse(initial_source).unwrap().0.unwrap();
8481
8482        let mut frontend = FrontendState::new();
8483
8484        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8485        let mock_ctx = ExecutorContext::new_mock(None).await;
8486        let version = Version(0);
8487
8488        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490        let sketch_id = sketch_object.id;
8491        let sketch = expect_sketch(sketch_object);
8492
8493        let point_id = *sketch.segments.first().unwrap();
8494
8495        let point_ctor = PointCtor {
8496            position: Point2d {
8497                x: Expr::Var(Number {
8498                    value: 5.0,
8499                    units: NumericSuffix::Inch,
8500                }),
8501                y: Expr::Var(Number {
8502                    value: 6.0,
8503                    units: NumericSuffix::Inch,
8504                }),
8505            },
8506        };
8507        let segments = vec![ExistingSegmentCtor {
8508            id: point_id,
8509            ctor: SegmentCtor::Point(point_ctor),
8510        }];
8511        let (src_delta, scene_delta) = frontend
8512            .edit_segments(&mock_ctx, version, sketch_id, segments)
8513            .await
8514            .unwrap();
8515        assert_eq!(
8516            src_delta.text.as_str(),
8517            "\
8518sketch(on = XY) {
8519  line(start = [var 5in, var 6in], end = [var 3, var 4])
8520}
8521"
8522        );
8523        assert_eq!(scene_delta.new_objects, vec![]);
8524        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8525
8526        ctx.close().await;
8527        mock_ctx.close().await;
8528    }
8529
8530    #[tokio::test(flavor = "multi_thread")]
8531    async fn test_edit_line_when_editing_its_end_point() {
8532        let initial_source = "\
8533sketch(on = XY) {
8534  line(start = [var 1, var 2], end = [var 3, var 4])
8535}
8536";
8537
8538        let program = Program::parse(initial_source).unwrap().0.unwrap();
8539
8540        let mut frontend = FrontendState::new();
8541
8542        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8543        let mock_ctx = ExecutorContext::new_mock(None).await;
8544        let version = Version(0);
8545
8546        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8547        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8548        let sketch_id = sketch_object.id;
8549        let sketch = expect_sketch(sketch_object);
8550        let point_id = *sketch.segments.get(1).unwrap();
8551
8552        let point_ctor = PointCtor {
8553            position: Point2d {
8554                x: Expr::Var(Number {
8555                    value: 5.0,
8556                    units: NumericSuffix::Inch,
8557                }),
8558                y: Expr::Var(Number {
8559                    value: 6.0,
8560                    units: NumericSuffix::Inch,
8561                }),
8562            },
8563        };
8564        let segments = vec![ExistingSegmentCtor {
8565            id: point_id,
8566            ctor: SegmentCtor::Point(point_ctor),
8567        }];
8568        let (src_delta, scene_delta) = frontend
8569            .edit_segments(&mock_ctx, version, sketch_id, segments)
8570            .await
8571            .unwrap();
8572        assert_eq!(
8573            src_delta.text.as_str(),
8574            "\
8575sketch(on = XY) {
8576  line(start = [var 1, var 2], end = [var 5in, var 6in])
8577}
8578"
8579        );
8580        assert_eq!(scene_delta.new_objects, vec![]);
8581        assert_eq!(
8582            scene_delta.new_graph.objects.len(),
8583            5,
8584            "{:#?}",
8585            scene_delta.new_graph.objects
8586        );
8587
8588        ctx.close().await;
8589        mock_ctx.close().await;
8590    }
8591
8592    #[tokio::test(flavor = "multi_thread")]
8593    async fn test_edit_line_with_coincident_feedback() {
8594        let initial_source = "\
8595sketch(on = XY) {
8596  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8597  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8598  fixed([line1.start, [0, 0]])
8599  coincident([line1.end, line2.start])
8600  equalLength([line1, line2])
8601}
8602";
8603
8604        let program = Program::parse(initial_source).unwrap().0.unwrap();
8605
8606        let mut frontend = FrontendState::new();
8607
8608        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8609        let mock_ctx = ExecutorContext::new_mock(None).await;
8610        let version = Version(0);
8611
8612        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8613        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8614        let sketch_id = sketch_object.id;
8615        let sketch = expect_sketch(sketch_object);
8616        let line2_end_id = *sketch.segments.get(4).unwrap();
8617
8618        let segments = vec![ExistingSegmentCtor {
8619            id: line2_end_id,
8620            ctor: SegmentCtor::Point(PointCtor {
8621                position: Point2d {
8622                    x: Expr::Var(Number {
8623                        value: 9.0,
8624                        units: NumericSuffix::None,
8625                    }),
8626                    y: Expr::Var(Number {
8627                        value: 10.0,
8628                        units: NumericSuffix::None,
8629                    }),
8630                },
8631            }),
8632        }];
8633        let (src_delta, scene_delta) = frontend
8634            .edit_segments(&mock_ctx, version, sketch_id, segments)
8635            .await
8636            .unwrap();
8637        assert_eq!(
8638            src_delta.text.as_str(),
8639            "\
8640sketch(on = XY) {
8641  line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8642  line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8643  fixed([line1.start, [0, 0]])
8644  coincident([line1.end, line2.start])
8645  equalLength([line1, line2])
8646}
8647"
8648        );
8649        assert_eq!(
8650            scene_delta.new_graph.objects.len(),
8651            11,
8652            "{:#?}",
8653            scene_delta.new_graph.objects
8654        );
8655
8656        ctx.close().await;
8657        mock_ctx.close().await;
8658    }
8659
8660    #[tokio::test(flavor = "multi_thread")]
8661    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8662        let initial_source = "\
8663sketch(on = XY) {
8664  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8665  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8666  fixed([line1.start, [0, 0]])
8667  coincident([line1.end, line2.start])
8668  equalLength([line1, line2])
8669}
8670";
8671
8672        let program = Program::parse(initial_source).unwrap().0.unwrap();
8673        let mut frontend = FrontendState::new();
8674        let mock_ctx = ExecutorContext::new_mock(None).await;
8675        let version = Version(0);
8676
8677        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8678        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8679        let sketch_id = sketch_object.id;
8680        let sketch = expect_sketch(sketch_object);
8681        let line2_end_id = *sketch.segments.get(4).unwrap();
8682
8683        let segments = vec![ExistingSegmentCtor {
8684            id: line2_end_id,
8685            ctor: SegmentCtor::Point(PointCtor {
8686                position: Point2d {
8687                    x: Expr::Var(Number {
8688                        value: 9.0,
8689                        units: NumericSuffix::None,
8690                    }),
8691                    y: Expr::Var(Number {
8692                        value: 10.0,
8693                        units: NumericSuffix::None,
8694                    }),
8695                },
8696            }),
8697        }];
8698        let (edited_source, _) = frontend
8699            .edit_segments(&mock_ctx, version, sketch_id, segments)
8700            .await
8701            .unwrap();
8702
8703        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8704        assert_eq!(mock_source.text, edited_source.text);
8705
8706        mock_ctx.close().await;
8707    }
8708
8709    /// Preview segment edits should return solved geometry without persisting
8710    /// solver feedback to KCL.
8711    #[tokio::test(flavor = "multi_thread")]
8712    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8713        let initial_source = "\
8714sketch(on = XY) {
8715  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8716  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8717  fixed([line1.start, [0, 0]])
8718  coincident([line1.end, line2.start])
8719  equalLength([line1, line2])
8720}
8721";
8722
8723        let program = Program::parse(initial_source).unwrap().0.unwrap();
8724        let mut frontend = FrontendState::new();
8725        let mock_ctx = ExecutorContext::new_mock(None).await;
8726        let version = Version(0);
8727
8728        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8729        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8730        let sketch_id = sketch_object.id;
8731        let sketch = expect_sketch(sketch_object);
8732        let line2_end_id = *sketch.segments.get(4).unwrap();
8733
8734        let segments = vec![ExistingSegmentCtor {
8735            id: line2_end_id,
8736            ctor: SegmentCtor::Point(PointCtor {
8737                position: Point2d {
8738                    x: Expr::Var(Number {
8739                        value: 9.0,
8740                        units: NumericSuffix::None,
8741                    }),
8742                    y: Expr::Var(Number {
8743                        value: 10.0,
8744                        units: NumericSuffix::None,
8745                    }),
8746                },
8747            }),
8748        }];
8749        let (preview_source, preview_delta) = frontend
8750            .edit_segments_with_options(
8751                &mock_ctx,
8752                version,
8753                sketch_id,
8754                segments,
8755                EditSegmentsOptions {
8756                    anchor_segment_ids: Some(vec![line2_end_id]),
8757                    drag_anchors: Vec::new(),
8758                    commit_solved_initial_guesses: false,
8759                },
8760            )
8761            .await
8762            .unwrap();
8763
8764        assert!(
8765            !preview_delta.exec_outcome.var_solutions.is_empty(),
8766            "preview solve should still solve and return geometry feedback"
8767        );
8768        assert!(
8769            preview_source
8770                .text
8771                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8772        );
8773        assert!(
8774            preview_source
8775                .text
8776                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8777        );
8778
8779        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8780        assert_eq!(mock_source.text, preview_source.text);
8781
8782        mock_ctx.close().await;
8783    }
8784
8785    #[tokio::test(flavor = "multi_thread")]
8786    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8787        let initial_source = "\
8788sketch(on = XY) {
8789  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8790}
8791";
8792
8793        let program = Program::parse(initial_source).unwrap().0.unwrap();
8794        let mut frontend = FrontendState::new();
8795        let mock_ctx = ExecutorContext::new_mock(None).await;
8796        let version = Version(0);
8797
8798        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8799        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8800        let sketch_id = sketch_object.id;
8801        let sketch = expect_sketch(sketch_object);
8802        let line_end_id = *sketch.segments.get(1).unwrap();
8803
8804        let constraint = Constraint::Fixed(Fixed {
8805            points: vec![FixedPoint {
8806                point: line_end_id,
8807                position: Point2d {
8808                    x: Number {
8809                        value: 20.0,
8810                        units: NumericSuffix::Mm,
8811                    },
8812                    y: Number {
8813                        value: 0.0,
8814                        units: NumericSuffix::Mm,
8815                    },
8816                },
8817            }],
8818        });
8819        let (constraint_source, _) = frontend
8820            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8821            .await
8822            .unwrap();
8823
8824        assert!(
8825            constraint_source
8826                .text
8827                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8828            "{}",
8829            constraint_source.text
8830        );
8831        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8832        assert_eq!(mock_source.text, constraint_source.text);
8833
8834        mock_ctx.close().await;
8835    }
8836
8837    #[test]
8838    fn test_no_solver_feedback_preserves_original_source() {
8839        let initial_source = "\
8840@settings(defaultLengthUnit = in, kclVersion = 2.0)
8841cylinder = startSketchOn(XY)
8842    |> circle(center= [0, 0], radius= 22)
8843    |> extrude(length = 14)
8844";
8845        let mut frontend = FrontendState::new();
8846        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8847        let outcome = ExecOutcome {
8848            variables: Default::default(),
8849            operations: Default::default(),
8850            artifact_graph: Default::default(),
8851            scene_objects: Default::default(),
8852            source_range_to_object: Default::default(),
8853            var_solutions: Default::default(),
8854            issues: Default::default(),
8855            filenames: Default::default(),
8856            default_planes: Default::default(),
8857        };
8858
8859        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8860
8861        assert_eq!(source_delta.text, initial_source);
8862    }
8863
8864    /// Explicit drag anchors should limit which edited points become temporary
8865    /// fixed constraints.
8866    #[tokio::test(flavor = "multi_thread")]
8867    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8868        let initial_source = "\
8869sketch(on = XY) {
8870  point1 = point(at = [var 0mm, var 0mm])
8871  point2 = point(at = [var 0mm, var 0mm])
8872  coincident([point1, point2])
8873}
8874";
8875
8876        let program = Program::parse(initial_source).unwrap().0.unwrap();
8877        let mut frontend = FrontendState::new();
8878        let mock_ctx = ExecutorContext::new_mock(None).await;
8879        let version = Version(0);
8880
8881        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8882        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8883        let sketch_id = sketch_object.id;
8884        let sketch = expect_sketch(sketch_object);
8885        let point1_id = sketch.segments[0];
8886        let point2_id = sketch.segments[1];
8887
8888        let segments = vec![
8889            ExistingSegmentCtor {
8890                id: point1_id,
8891                ctor: SegmentCtor::Point(PointCtor {
8892                    position: point_expr_mm(10.0, 0.0),
8893                }),
8894            },
8895            ExistingSegmentCtor {
8896                id: point2_id,
8897                ctor: SegmentCtor::Point(PointCtor {
8898                    position: point_expr_mm(100.0, 0.0),
8899                }),
8900            },
8901        ];
8902        let (_, scene_delta) = frontend
8903            .edit_segments_with_options(
8904                &mock_ctx,
8905                version,
8906                sketch_id,
8907                segments,
8908                EditSegmentsOptions {
8909                    anchor_segment_ids: Some(vec![point1_id]),
8910                    drag_anchors: Vec::new(),
8911                    commit_solved_initial_guesses: true,
8912                },
8913            )
8914            .await
8915            .unwrap();
8916
8917        assert_point_position_close(
8918            point_position(&scene_delta.new_graph, point1_id),
8919            point_number_mm(10.0, 0.0),
8920        );
8921        assert_point_position_close(
8922            point_position(&scene_delta.new_graph, point2_id),
8923            point_number_mm(10.0, 0.0),
8924        );
8925
8926        mock_ctx.close().await;
8927    }
8928
8929    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
8930    /// for every SketchVar whose initial NumericLiteral has the given value.
8931    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8932        use std::cell::RefCell;
8933        struct Collector {
8934            target: f64,
8935            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8936        }
8937        impl<'a> crate::walk::Visitor<'a> for &Collector {
8938            type Error = crate::front::Error;
8939            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8940                if let crate::walk::Node::SketchVar(sketch_var) = node
8941                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8942                    && (initial.value - self.target).abs() < 1e-9
8943                {
8944                    self.out
8945                        .borrow_mut()
8946                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8947                }
8948                for child in node.children().iter() {
8949                    if !child.visit(*self)? {
8950                        return Ok(false);
8951                    }
8952                }
8953                Ok(true)
8954            }
8955        }
8956        let collector = Collector {
8957            target: value,
8958            out: Default::default(),
8959        };
8960        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8961        collector.out.into_inner()
8962    }
8963
8964    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
8965    /// for every SketchVar (including bare `var`).
8966    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8967        use std::cell::RefCell;
8968        struct Collector {
8969            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8970        }
8971        impl<'a> crate::walk::Visitor<'a> for &Collector {
8972            type Error = crate::front::Error;
8973            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8974                if let crate::walk::Node::SketchVar(sketch_var) = node
8975                    && let Some(node_path) = &sketch_var.node_path
8976                {
8977                    self.out
8978                        .borrow_mut()
8979                        .push((SourceRange::from(sketch_var), node_path.clone()));
8980                }
8981                for child in node.children().iter() {
8982                    if !child.visit(*self)? {
8983                        return Ok(false);
8984                    }
8985                }
8986                Ok(true)
8987            }
8988        }
8989        let collector = Collector {
8990            out: Default::default(),
8991        };
8992        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8993        collector.out.into_inner()
8994    }
8995
8996    fn empty_exec_outcome_with_var_solutions(
8997        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8998    ) -> ExecOutcome {
8999        ExecOutcome {
9000            variables: Default::default(),
9001            operations: Default::default(),
9002            artifact_graph: Default::default(),
9003            scene_objects: Default::default(),
9004            source_range_to_object: Default::default(),
9005            var_solutions,
9006            issues: Default::default(),
9007            filenames: Default::default(),
9008            default_planes: Default::default(),
9009        }
9010    }
9011
9012    /// Happy path: commit a var solution to a `var N` inside a sketch block
9013    /// using a correct NodePath. Confirms the node-path code path produces the
9014    /// expected source mutation.
9015    #[test]
9016    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9017        let initial_source = "\
9018sketch(on = XY) {
9019  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9020}
9021";
9022        let program = Program::parse(initial_source).unwrap().0.unwrap();
9023        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9024        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9025        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9026
9027        let mut frontend = FrontendState::new();
9028        frontend.program = program;
9029
9030        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9031            literal_range,
9032            Some(node_path),
9033            Number {
9034                value: 25.0,
9035                units: NumericSuffix::Mm,
9036            },
9037        )]);
9038
9039        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9040
9041        assert_eq!(
9042            source_delta.text,
9043            "\
9044sketch(on = XY) {
9045  line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9046}
9047",
9048        );
9049    }
9050
9051    /// Whitespace inserted earlier in the source shifts the original SketchVar
9052    /// SourceRange. With NodePath propagation the commit should still target
9053    /// the right `var`. We simulate this by collecting node_paths against a
9054    /// "compact" source, then loading the frontend with a "padded" source
9055    /// (whose byte offsets differ), and feeding the original (now stale)
9056    /// source range plus the correct node_path back into the commit.
9057    #[test]
9058    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9059        let compact_source = "\
9060sketch(on = XY) {
9061  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9062}
9063";
9064        let padded_source = "\
9065// added comment\n// added comment\n\nsketch(on = XY) {
9066  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9067}
9068";
9069        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9070        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9071
9072        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9073            .into_iter()
9074            .next()
9075            .expect("expected `var 10mm` in compact source");
9076        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9077            .into_iter()
9078            .next()
9079            .expect("expected `var 10mm` in padded source");
9080
9081        assert_ne!(
9082            compact_match.0, padded_match.0,
9083            "byte offsets must differ for this test to be meaningful"
9084        );
9085        assert_eq!(
9086            compact_match.1, padded_match.1,
9087            "node paths must agree across whitespace; that's the whole point of NodePath",
9088        );
9089
9090        let mut frontend = FrontendState::new();
9091        frontend.program = padded_program;
9092
9093        // Stale source range from the compact source + correct node_path.
9094        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9095            compact_match.0,
9096            Some(compact_match.1),
9097            Number {
9098                value: 30.0,
9099                units: NumericSuffix::Mm,
9100            },
9101        )]);
9102
9103        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9104
9105        assert_eq!(
9106            source_delta.text,
9107            "\
9108// added comment
9109// added comment
9110
9111sketch(on = XY) {
9112  line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9113}
9114",
9115        );
9116    }
9117
9118    /// When multiple `var` declarations exist and the stale source range
9119    /// happens to land on a *different* var, the node_path must take
9120    /// precedence and the right var gets updated.
9121    #[test]
9122    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9123        let initial_source = "\
9124sketch(on = XY) {
9125  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9126}
9127";
9128        let program = Program::parse(initial_source).unwrap().0.unwrap();
9129
9130        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9131            .into_iter()
9132            .next()
9133            .expect("expected `var 10mm`");
9134        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9135            .into_iter()
9136            .next()
9137            .expect("expected `var 20mm`");
9138
9139        let mut frontend = FrontendState::new();
9140        frontend.program = program;
9141
9142        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9143        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9144            var_20.0,
9145            Some(var_10.1),
9146            Number {
9147                value: 33.0,
9148                units: NumericSuffix::Mm,
9149            },
9150        )]);
9151
9152        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9153
9154        assert_eq!(
9155            source_delta.text,
9156            "\
9157sketch(on = XY) {
9158  line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9159}
9160",
9161        );
9162    }
9163
9164    /// Bare `var` (no initial literal) is only locatable via node_path. With
9165    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9166    /// solver solution should fill the initial value in. The
9167    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9168    /// is gated as an experimental feature; without it the re-parse of the
9169    /// recast source rejects bare `var` declarations.
9170    #[test]
9171    fn test_commit_var_solution_writes_back_into_bare_var() {
9172        let initial_source = "\
9173@settings(experimentalFeatures = allow, kclVersion = 2.0)
9174sketch(on = XY) {
9175  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9176}
9177";
9178        let program = Program::parse(initial_source).unwrap().0.unwrap();
9179
9180        // Pick the first bare `var`; collect_all_sketch_vars returns every
9181        // SketchVar, including bare ones.
9182        let bare = collect_all_sketch_vars(&program)
9183            .into_iter()
9184            .find(|(range, _)| {
9185                // The bare `var` is exactly the 3 characters "var".
9186                range.end() - range.start() == 3
9187            })
9188            .expect("expected at least one bare `var`");
9189
9190        let mut frontend = FrontendState::new();
9191        frontend.program = program;
9192
9193        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9194            bare.0,
9195            Some(bare.1),
9196            Number {
9197                value: 7.0,
9198                units: NumericSuffix::Mm,
9199            },
9200        )]);
9201
9202        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9203
9204        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9205        // written as an explicit suffix so the bare var commits with units.
9206        // The recast adds a blank line after the `@settings` annotation.
9207        assert_eq!(
9208            source_delta.text,
9209            "\
9210@settings(experimentalFeatures = allow, kclVersion = 2.0)
9211
9212sketch(on = XY) {
9213  line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9214}
9215",
9216        );
9217    }
9218
9219    #[tokio::test(flavor = "multi_thread")]
9220    async fn test_delete_point_without_var() {
9221        let initial_source = "\
9222sketch(on = XY) {
9223  point(at = [var 1, var 2])
9224  point(at = [var 3, var 4])
9225  point(at = [var 5, var 6])
9226}
9227";
9228
9229        let program = Program::parse(initial_source).unwrap().0.unwrap();
9230
9231        let mut frontend = FrontendState::new();
9232
9233        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9234        let mock_ctx = ExecutorContext::new_mock(None).await;
9235        let version = Version(0);
9236
9237        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9238        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9239        let sketch_id = sketch_object.id;
9240        let sketch = expect_sketch(sketch_object);
9241
9242        let point_id = *sketch.segments.get(1).unwrap();
9243
9244        let (src_delta, scene_delta) = frontend
9245            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9246            .await
9247            .unwrap();
9248        assert_eq!(
9249            src_delta.text.as_str(),
9250            "\
9251sketch(on = XY) {
9252  point(at = [var 1, var 2])
9253  point(at = [var 5, var 6])
9254}
9255"
9256        );
9257        assert_eq!(scene_delta.new_objects, vec![]);
9258        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9259
9260        ctx.close().await;
9261        mock_ctx.close().await;
9262    }
9263
9264    #[tokio::test(flavor = "multi_thread")]
9265    async fn test_delete_point_with_var() {
9266        let initial_source = "\
9267sketch(on = XY) {
9268  point(at = [var 1, var 2])
9269  point1 = point(at = [var 3, var 4])
9270  point(at = [var 5, var 6])
9271}
9272";
9273
9274        let program = Program::parse(initial_source).unwrap().0.unwrap();
9275
9276        let mut frontend = FrontendState::new();
9277
9278        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9279        let mock_ctx = ExecutorContext::new_mock(None).await;
9280        let version = Version(0);
9281
9282        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9283        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9284        let sketch_id = sketch_object.id;
9285        let sketch = expect_sketch(sketch_object);
9286
9287        let point_id = *sketch.segments.get(1).unwrap();
9288
9289        let (src_delta, scene_delta) = frontend
9290            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9291            .await
9292            .unwrap();
9293        assert_eq!(
9294            src_delta.text.as_str(),
9295            "\
9296sketch(on = XY) {
9297  point(at = [var 1, var 2])
9298  point(at = [var 5, var 6])
9299}
9300"
9301        );
9302        assert_eq!(scene_delta.new_objects, vec![]);
9303        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9304
9305        ctx.close().await;
9306        mock_ctx.close().await;
9307    }
9308
9309    #[tokio::test(flavor = "multi_thread")]
9310    async fn test_delete_multiple_points() {
9311        let initial_source = "\
9312sketch(on = XY) {
9313  point(at = [var 1, var 2])
9314  point1 = point(at = [var 3, var 4])
9315  point(at = [var 5, var 6])
9316}
9317";
9318
9319        let program = Program::parse(initial_source).unwrap().0.unwrap();
9320
9321        let mut frontend = FrontendState::new();
9322
9323        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9324        let mock_ctx = ExecutorContext::new_mock(None).await;
9325        let version = Version(0);
9326
9327        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9328        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9329        let sketch_id = sketch_object.id;
9330
9331        let sketch = expect_sketch(sketch_object);
9332
9333        let point1_id = *sketch.segments.first().unwrap();
9334        let point2_id = *sketch.segments.get(1).unwrap();
9335
9336        let (src_delta, scene_delta) = frontend
9337            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9338            .await
9339            .unwrap();
9340        assert_eq!(
9341            src_delta.text.as_str(),
9342            "\
9343sketch(on = XY) {
9344  point(at = [var 5, var 6])
9345}
9346"
9347        );
9348        assert_eq!(scene_delta.new_objects, vec![]);
9349        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9350
9351        ctx.close().await;
9352        mock_ctx.close().await;
9353    }
9354
9355    #[tokio::test(flavor = "multi_thread")]
9356    async fn test_delete_coincident_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  coincident([point1, point2])
9362  point(at = [var 5, var 6])
9363}
9364";
9365
9366        let program = Program::parse(initial_source).unwrap().0.unwrap();
9367
9368        let mut frontend = FrontendState::new();
9369
9370        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9371        let mock_ctx = ExecutorContext::new_mock(None).await;
9372        let version = Version(0);
9373
9374        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
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
9379        let coincident_id = *sketch.constraints.first().unwrap();
9380
9381        let (src_delta, scene_delta) = frontend
9382            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9383            .await
9384            .unwrap();
9385        assert_eq!(
9386            src_delta.text.as_str(),
9387            "\
9388sketch(on = XY) {
9389  point1 = point(at = [var 1, var 2])
9390  point2 = point(at = [var 3, var 4])
9391  point(at = [var 5, var 6])
9392}
9393"
9394        );
9395        assert_eq!(scene_delta.new_objects, vec![]);
9396        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9397
9398        ctx.close().await;
9399        mock_ctx.close().await;
9400    }
9401
9402    #[tokio::test(flavor = "multi_thread")]
9403    async fn test_delete_line_cascades_to_coincident_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  coincident([line1.end, line2.start])
9409}
9410";
9411
9412        let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414        let mut frontend = FrontendState::new();
9415
9416        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9417        let mock_ctx = ExecutorContext::new_mock(None).await;
9418        let version = Version(0);
9419
9420        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9421        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9422        let sketch_id = sketch_object.id;
9423        let sketch = expect_sketch(sketch_object);
9424        let line_id = *sketch.segments.get(5).unwrap();
9425
9426        let (src_delta, scene_delta) = frontend
9427            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9428            .await
9429            .unwrap();
9430        assert_eq!(
9431            src_delta.text.as_str(),
9432            "\
9433sketch(on = XY) {
9434  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9435}
9436"
9437        );
9438        assert_eq!(
9439            scene_delta.new_graph.objects.len(),
9440            5,
9441            "{:#?}",
9442            scene_delta.new_graph.objects
9443        );
9444
9445        ctx.close().await;
9446        mock_ctx.close().await;
9447    }
9448
9449    #[tokio::test(flavor = "multi_thread")]
9450    async fn test_delete_line_cascades_to_distance_constraint() {
9451        let initial_source = "\
9452sketch(on = XY) {
9453  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9454  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9455  distance([line1.end, line2.start]) == 10mm
9456}
9457";
9458
9459        let program = Program::parse(initial_source).unwrap().0.unwrap();
9460
9461        let mut frontend = FrontendState::new();
9462
9463        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9464        let mock_ctx = ExecutorContext::new_mock(None).await;
9465        let version = Version(0);
9466
9467        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9468        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9469        let sketch_id = sketch_object.id;
9470        let sketch = expect_sketch(sketch_object);
9471        let line_id = *sketch.segments.get(5).unwrap();
9472
9473        let (src_delta, scene_delta) = frontend
9474            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9475            .await
9476            .unwrap();
9477        assert_eq!(
9478            src_delta.text.as_str(),
9479            "\
9480sketch(on = XY) {
9481  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9482}
9483"
9484        );
9485        assert_eq!(
9486            scene_delta.new_graph.objects.len(),
9487            5,
9488            "{:#?}",
9489            scene_delta.new_graph.objects
9490        );
9491
9492        ctx.close().await;
9493        mock_ctx.close().await;
9494    }
9495
9496    #[tokio::test(flavor = "multi_thread")]
9497    async fn test_delete_point_cascades_to_horizontal_distance_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  horizontalDistance([point1, point2]) == 10mm
9503}
9504";
9505
9506        let program = Program::parse(initial_source).unwrap().0.unwrap();
9507
9508        let mut frontend = FrontendState::new();
9509
9510        let mock_ctx = ExecutorContext::new_mock(None).await;
9511        let version = Version(0);
9512
9513        frontend.program = program.clone();
9514        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9515        frontend.update_state_after_exec(outcome, true);
9516        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9517        let sketch_id = sketch_object.id;
9518        let sketch = expect_sketch(sketch_object);
9519        let point2_id = *sketch.segments.get(1).unwrap();
9520
9521        let (src_delta, scene_delta) = frontend
9522            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9523            .await
9524            .unwrap();
9525        assert_eq!(
9526            src_delta.text.as_str(),
9527            "\
9528sketch(on = XY) {
9529  point1 = point(at = [var 1, var 2])
9530}
9531"
9532        );
9533        assert_eq!(
9534            scene_delta.new_graph.objects.len(),
9535            3,
9536            "{:#?}",
9537            scene_delta.new_graph.objects
9538        );
9539
9540        mock_ctx.close().await;
9541    }
9542
9543    #[tokio::test(flavor = "multi_thread")]
9544    async fn test_delete_line_cascades_to_fixed_constraint() {
9545        let initial_source = "\
9546sketch(on = XY) {
9547  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9548  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9549  fixed([line1.start, [0, 0]])
9550}
9551";
9552
9553        let program = Program::parse(initial_source).unwrap().0.unwrap();
9554
9555        let mut frontend = FrontendState::new();
9556
9557        let mock_ctx = ExecutorContext::new_mock(None).await;
9558        let version = Version(0);
9559
9560        frontend.program = program.clone();
9561        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9562        frontend.update_state_after_exec(outcome, true);
9563        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9564        let sketch_id = sketch_object.id;
9565        let sketch = expect_sketch(sketch_object);
9566        let line1_id = *sketch.segments.get(2).unwrap();
9567
9568        let (src_delta, scene_delta) = frontend
9569            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9570            .await
9571            .unwrap();
9572        assert_eq!(
9573            src_delta.text.as_str(),
9574            "\
9575sketch(on = XY) {
9576  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9577}
9578"
9579        );
9580        assert_eq!(
9581            scene_delta.new_graph.objects.len(),
9582            5,
9583            "{:#?}",
9584            scene_delta.new_graph.objects
9585        );
9586
9587        mock_ctx.close().await;
9588    }
9589
9590    #[tokio::test(flavor = "multi_thread")]
9591    async fn test_delete_line_cascades_to_midpoint_constraint() {
9592        let initial_source = "\
9593sketch(on = XY) {
9594  point1 = point(at = [var 1, var 2])
9595  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9596  midpoint(line1, point = point1)
9597}
9598";
9599
9600        let program = Program::parse(initial_source).unwrap().0.unwrap();
9601
9602        let mut frontend = FrontendState::new();
9603
9604        let mock_ctx = ExecutorContext::new_mock(None).await;
9605        let version = Version(0);
9606
9607        frontend.program = program.clone();
9608        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9609        frontend.update_state_after_exec(outcome, true);
9610        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9611        let sketch_id = sketch_object.id;
9612        let sketch = expect_sketch(sketch_object);
9613        let line1_id = *sketch.segments.get(3).unwrap();
9614
9615        let (src_delta, scene_delta) = frontend
9616            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9617            .await
9618            .unwrap();
9619        assert_eq!(
9620            src_delta.text.as_str(),
9621            "\
9622sketch(on = XY) {
9623  point1 = point(at = [var 1, var 2])
9624}
9625"
9626        );
9627        assert_eq!(
9628            scene_delta.new_graph.objects.len(),
9629            3,
9630            "{:#?}",
9631            scene_delta.new_graph.objects
9632        );
9633
9634        mock_ctx.close().await;
9635    }
9636
9637    #[tokio::test(flavor = "multi_thread")]
9638    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9639        let initial_source = "\
9640sketch(on = XY) {
9641  point1 = point(at = [var 1, var 2])
9642  point2 = point(at = [var 3, var 4])
9643  point3 = point(at = [var 5, var 6])
9644  coincident([point1, point2, point3])
9645}
9646";
9647
9648        let program = Program::parse(initial_source).unwrap().0.unwrap();
9649
9650        let mut frontend = FrontendState::new();
9651
9652        let mock_ctx = ExecutorContext::new_mock(None).await;
9653        let version = Version(0);
9654
9655        frontend.program = program.clone();
9656        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9657        frontend.update_state_after_exec(outcome, true);
9658        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9659        let sketch_id = sketch_object.id;
9660        let sketch = expect_sketch(sketch_object);
9661        let point3_id = *sketch.segments.get(2).unwrap();
9662
9663        let (src_delta, scene_delta) = frontend
9664            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9665            .await
9666            .unwrap();
9667        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9668        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9669        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9670        assert!(
9671            src_delta.text.contains("coincident([point1, point2])"),
9672            "{}",
9673            src_delta.text
9674        );
9675
9676        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9677        let sketch = expect_sketch(sketch_object);
9678        assert_eq!(sketch.segments.len(), 2);
9679        assert_eq!(sketch.constraints.len(), 1);
9680
9681        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9682        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9683            panic!("Expected constraint object");
9684        };
9685        let Constraint::Coincident(coincident) = constraint else {
9686            panic!("Expected coincident constraint");
9687        };
9688        assert_eq!(
9689            coincident.segments,
9690            sketch
9691                .segments
9692                .iter()
9693                .copied()
9694                .map(Into::into)
9695                .collect::<Vec<ConstraintSegment>>()
9696        );
9697
9698        mock_ctx.close().await;
9699    }
9700
9701    #[tokio::test(flavor = "multi_thread")]
9702    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9703        let initial_source = "\
9704sketch(on = XY) {
9705  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9706  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9707  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9708  equalLength([line1, line2, line3])
9709}
9710";
9711
9712        let program = Program::parse(initial_source).unwrap().0.unwrap();
9713
9714        let mut frontend = FrontendState::new();
9715
9716        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9717        let mock_ctx = ExecutorContext::new_mock(None).await;
9718        let version = Version(0);
9719
9720        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9721        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9722        let sketch_id = sketch_object.id;
9723        let sketch = expect_sketch(sketch_object);
9724        let line3_id = *sketch.segments.get(8).unwrap();
9725
9726        let (src_delta, scene_delta) = frontend
9727            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9728            .await
9729            .unwrap();
9730        assert_eq!(
9731            src_delta.text.as_str(),
9732            "\
9733sketch(on = XY) {
9734  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9735  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9736  equalLength([line1, line2])
9737}
9738"
9739        );
9740
9741        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9742        let sketch = expect_sketch(sketch_object);
9743        assert_eq!(sketch.constraints.len(), 1);
9744
9745        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9746        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9747            panic!("Expected constraint object");
9748        };
9749        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9750            panic!("Expected lines equal length constraint");
9751        };
9752        assert_eq!(lines_equal_length.lines.len(), 2);
9753
9754        ctx.close().await;
9755        mock_ctx.close().await;
9756    }
9757
9758    #[tokio::test(flavor = "multi_thread")]
9759    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9760        let initial_source = "\
9761sketch(on = XY) {
9762  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9763  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9764  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9765  horizontal([line1.end, line2.start, line3.start])
9766}
9767";
9768
9769        let program = Program::parse(initial_source).unwrap().0.unwrap();
9770
9771        let mut frontend = FrontendState::new();
9772
9773        let mock_ctx = ExecutorContext::new_mock(None).await;
9774        let version = Version(0);
9775
9776        frontend.program = program.clone();
9777        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9778        frontend.update_state_after_exec(outcome, true);
9779        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9780        let sketch_id = sketch_object.id;
9781        let sketch = expect_sketch(sketch_object);
9782        let line1_id = *sketch.segments.get(2).unwrap();
9783
9784        let (src_delta, scene_delta) = frontend
9785            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9786            .await
9787            .unwrap();
9788        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9789        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9790        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9791        assert!(
9792            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9793            "{}",
9794            src_delta.text
9795        );
9796
9797        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9798        let sketch = expect_sketch(sketch_object);
9799        assert_eq!(sketch.constraints.len(), 1);
9800
9801        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9802        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9803            panic!("Expected constraint object");
9804        };
9805        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9806            panic!("Expected horizontal points constraint");
9807        };
9808        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9809        assert_eq!(*points, remaining_points);
9810
9811        mock_ctx.close().await;
9812    }
9813
9814    #[tokio::test(flavor = "multi_thread")]
9815    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9816        let initial_source = "\
9817sketch(on = XY) {
9818  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9819  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9820  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9821  vertical([line1.end, line2.start, line3.start])
9822}
9823";
9824
9825        let program = Program::parse(initial_source).unwrap().0.unwrap();
9826
9827        let mut frontend = FrontendState::new();
9828
9829        let mock_ctx = ExecutorContext::new_mock(None).await;
9830        let version = Version(0);
9831
9832        frontend.program = program.clone();
9833        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9834        frontend.update_state_after_exec(outcome, true);
9835        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9836        let sketch_id = sketch_object.id;
9837        let sketch = expect_sketch(sketch_object);
9838        let line1_id = *sketch.segments.get(2).unwrap();
9839
9840        let (src_delta, scene_delta) = frontend
9841            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9842            .await
9843            .unwrap();
9844        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9845        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9846        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9847        assert!(
9848            src_delta.text.contains("vertical([line2.start, line3.start])"),
9849            "{}",
9850            src_delta.text
9851        );
9852
9853        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9854        let sketch = expect_sketch(sketch_object);
9855        assert_eq!(sketch.constraints.len(), 1);
9856
9857        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9858        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9859            panic!("Expected constraint object");
9860        };
9861        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9862            panic!("Expected vertical points constraint");
9863        };
9864        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9865        assert_eq!(*points, remaining_points);
9866
9867        mock_ctx.close().await;
9868    }
9869
9870    #[tokio::test(flavor = "multi_thread")]
9871    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9872        let initial_source = "\
9873sketch(on = XY) {
9874  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9875  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9876  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9877  coincident([line1.end, line2.start, line3.start])
9878}
9879";
9880
9881        let program = Program::parse(initial_source).unwrap().0.unwrap();
9882
9883        let mut frontend = FrontendState::new();
9884
9885        let mock_ctx = ExecutorContext::new_mock(None).await;
9886        let version = Version(0);
9887
9888        frontend.program = program.clone();
9889        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9890        frontend.update_state_after_exec(outcome, true);
9891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892        let sketch_id = sketch_object.id;
9893        let sketch = expect_sketch(sketch_object);
9894        let line1_id = *sketch.segments.get(2).unwrap();
9895
9896        let (src_delta, scene_delta) = frontend
9897            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9898            .await
9899            .unwrap();
9900        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9901        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9902        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9903        assert!(
9904            src_delta.text.contains("coincident([line2.start, line3.start])"),
9905            "{}",
9906            src_delta.text
9907        );
9908
9909        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9910        let sketch = expect_sketch(sketch_object);
9911        assert_eq!(sketch.constraints.len(), 1);
9912
9913        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9914        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9915            panic!("Expected constraint object");
9916        };
9917        let Constraint::Coincident(coincident) = constraint else {
9918            panic!("Expected coincident constraint");
9919        };
9920        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9921        assert_eq!(coincident.segments, remaining_segments);
9922
9923        mock_ctx.close().await;
9924    }
9925
9926    #[tokio::test(flavor = "multi_thread")]
9927    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9928        let initial_source = "\
9929sketch(on = XY) {
9930  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9931  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9932  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9933  equalLength([line1, line2, line3])
9934}
9935";
9936
9937        let program = Program::parse(initial_source).unwrap().0.unwrap();
9938
9939        let mut frontend = FrontendState::new();
9940
9941        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9942        let mock_ctx = ExecutorContext::new_mock(None).await;
9943        let version = Version(0);
9944
9945        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9946        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9947        let sketch_id = sketch_object.id;
9948        let sketch = expect_sketch(sketch_object);
9949        let line2_id = *sketch.segments.get(5).unwrap();
9950        let line3_id = *sketch.segments.get(8).unwrap();
9951
9952        let (src_delta, scene_delta) = frontend
9953            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9954            .await
9955            .unwrap();
9956        assert_eq!(
9957            src_delta.text.as_str(),
9958            "\
9959sketch(on = XY) {
9960  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9961}
9962"
9963        );
9964
9965        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9966        let sketch = expect_sketch(sketch_object);
9967        assert!(sketch.constraints.is_empty());
9968
9969        ctx.close().await;
9970        mock_ctx.close().await;
9971    }
9972
9973    #[tokio::test(flavor = "multi_thread")]
9974    async fn test_delete_line_preserves_multiline_parallel_constraint() {
9975        let initial_source = "\
9976sketch(on = XY) {
9977  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9978  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9979  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9980  parallel([line1, line2, line3])
9981}
9982";
9983
9984        let program = Program::parse(initial_source).unwrap().0.unwrap();
9985
9986        let mut frontend = FrontendState::new();
9987
9988        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9989        let mock_ctx = ExecutorContext::new_mock(None).await;
9990        let version = Version(0);
9991
9992        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9993        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9994        let sketch_id = sketch_object.id;
9995        let sketch = expect_sketch(sketch_object);
9996        let line3_id = *sketch.segments.get(8).unwrap();
9997
9998        let (src_delta, scene_delta) = frontend
9999            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10000            .await
10001            .unwrap();
10002        assert_eq!(
10003            src_delta.text.as_str(),
10004            "\
10005sketch(on = XY) {
10006  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10007  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10008  parallel([line1, line2])
10009}
10010"
10011        );
10012
10013        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10014        let sketch = expect_sketch(sketch_object);
10015        assert_eq!(sketch.constraints.len(), 1);
10016
10017        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10018        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10019            panic!("Expected constraint object");
10020        };
10021        let Constraint::Parallel(parallel) = constraint else {
10022            panic!("Expected parallel constraint");
10023        };
10024        assert_eq!(parallel.lines.len(), 2);
10025
10026        ctx.close().await;
10027        mock_ctx.close().await;
10028    }
10029
10030    #[tokio::test(flavor = "multi_thread")]
10031    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10032        let initial_source = "\
10033sketch(on = XY) {
10034  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10035  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10036  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10037  parallel([line1, line2, line3])
10038}
10039";
10040
10041        let program = Program::parse(initial_source).unwrap().0.unwrap();
10042
10043        let mut frontend = FrontendState::new();
10044
10045        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10046        let mock_ctx = ExecutorContext::new_mock(None).await;
10047        let version = Version(0);
10048
10049        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10050        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10051        let sketch_id = sketch_object.id;
10052        let sketch = expect_sketch(sketch_object);
10053        let line2_id = *sketch.segments.get(5).unwrap();
10054        let line3_id = *sketch.segments.get(8).unwrap();
10055
10056        let (src_delta, scene_delta) = frontend
10057            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10058            .await
10059            .unwrap();
10060        assert_eq!(
10061            src_delta.text.as_str(),
10062            "\
10063sketch(on = XY) {
10064  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10065}
10066"
10067        );
10068
10069        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10070        let sketch = expect_sketch(sketch_object);
10071        assert!(sketch.constraints.is_empty());
10072
10073        ctx.close().await;
10074        mock_ctx.close().await;
10075    }
10076
10077    #[tokio::test(flavor = "multi_thread")]
10078    async fn test_delete_line_line_coincident_constraint() {
10079        let initial_source = "\
10080sketch(on = XY) {
10081  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10082  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10083  coincident([line1, line2])
10084}
10085";
10086
10087        let program = Program::parse(initial_source).unwrap().0.unwrap();
10088
10089        let mut frontend = FrontendState::new();
10090
10091        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10092        let mock_ctx = ExecutorContext::new_mock(None).await;
10093        let version = Version(0);
10094
10095        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10096        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10097        let sketch_id = sketch_object.id;
10098        let sketch = expect_sketch(sketch_object);
10099
10100        let coincident_id = *sketch.constraints.first().unwrap();
10101
10102        let (src_delta, scene_delta) = frontend
10103            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10104            .await
10105            .unwrap();
10106        assert_eq!(
10107            src_delta.text.as_str(),
10108            "\
10109sketch(on = XY) {
10110  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10111  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10112}
10113"
10114        );
10115        assert_eq!(scene_delta.new_objects, vec![]);
10116        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10117
10118        ctx.close().await;
10119        mock_ctx.close().await;
10120    }
10121
10122    #[tokio::test(flavor = "multi_thread")]
10123    async fn test_two_points_coincident() {
10124        let initial_source = "\
10125sketch(on = XY) {
10126  point1 = point(at = [var 1, var 2])
10127  point(at = [3, 4])
10128}
10129";
10130
10131        let program = Program::parse(initial_source).unwrap().0.unwrap();
10132
10133        let mut frontend = FrontendState::new();
10134
10135        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10136        let mock_ctx = ExecutorContext::new_mock(None).await;
10137        let version = Version(0);
10138
10139        frontend.hack_set_program(&ctx, program).await.unwrap();
10140        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10141        let sketch_id = sketch_object.id;
10142        let sketch = expect_sketch(sketch_object);
10143        let point0_id = *sketch.segments.first().unwrap();
10144        let point1_id = *sketch.segments.get(1).unwrap();
10145
10146        let constraint = Constraint::Coincident(Coincident {
10147            segments: vec![point0_id.into(), point1_id.into()],
10148        });
10149        let (src_delta, scene_delta) = frontend
10150            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10151            .await
10152            .unwrap();
10153        assert_eq!(
10154            src_delta.text.as_str(),
10155            "\
10156sketch(on = XY) {
10157  point1 = point(at = [var 3, var 4])
10158  point2 = point(at = [3, 4])
10159  coincident([point1, point2])
10160}
10161"
10162        );
10163        assert_eq!(
10164            scene_delta.new_graph.objects.len(),
10165            5,
10166            "{:#?}",
10167            scene_delta.new_graph.objects
10168        );
10169
10170        ctx.close().await;
10171        mock_ctx.close().await;
10172    }
10173
10174    #[tokio::test(flavor = "multi_thread")]
10175    async fn test_three_points_coincident() {
10176        let initial_source = "\
10177sketch(on = XY) {
10178  point1 = point(at = [var 1, var 2])
10179  point(at = [var 3, var 4])
10180  point(at = [var 5, var 6])
10181}
10182";
10183
10184        let program = Program::parse(initial_source).unwrap().0.unwrap();
10185
10186        let mut frontend = FrontendState::new();
10187
10188        let mock_ctx = ExecutorContext::new_mock(None).await;
10189        let version = Version(0);
10190
10191        frontend.program = program.clone();
10192        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10193        frontend.update_state_after_exec(outcome, true);
10194        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10195        let sketch_id = sketch_object.id;
10196        let sketch = expect_sketch(sketch_object);
10197        let segments = sketch
10198            .segments
10199            .iter()
10200            .take(3)
10201            .copied()
10202            .map(Into::into)
10203            .collect::<Vec<ConstraintSegment>>();
10204
10205        let constraint = Constraint::Coincident(Coincident {
10206            segments: segments.clone(),
10207        });
10208        let (src_delta, scene_delta) = frontend
10209            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10210            .await
10211            .unwrap();
10212        assert_eq!(
10213            src_delta.text.as_str(),
10214            "\
10215sketch(on = XY) {
10216  point1 = point(at = [var 3, var 4])
10217  point2 = point(at = [var 3, var 4])
10218  point3 = point(at = [var 3, var 4])
10219  coincident([point1, point2, point3])
10220}
10221"
10222        );
10223
10224        let constraint_object = scene_delta
10225            .new_graph
10226            .objects
10227            .iter()
10228            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10229            .unwrap();
10230
10231        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10232            panic!("expected a constraint object");
10233        };
10234
10235        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10236
10237        mock_ctx.close().await;
10238    }
10239
10240    #[tokio::test(flavor = "multi_thread")]
10241    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10242        let initial_source = "\
10243sketch(on = XY) {
10244  point1 = point(at = [var 1, var 2])
10245  point2 = point(at = [var 3, var 4])
10246  point3 = point(at = [var 5, var 6])
10247  coincident([point1, point2, point3])
10248}
10249";
10250
10251        let program = Program::parse(initial_source).unwrap().0.unwrap();
10252
10253        let mut frontend = FrontendState::new();
10254
10255        let ctx = ExecutorContext::new_mock(None).await;
10256        frontend.program = program.clone();
10257        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10258        frontend.update_state_after_exec(outcome, true);
10259
10260        let constraint_object = frontend
10261            .scene_graph
10262            .objects
10263            .iter()
10264            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10265            .unwrap();
10266        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10267            panic!("expected a constraint object");
10268        };
10269
10270        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10271        let sketch = expect_sketch(sketch_object);
10272        let expected_segments = sketch
10273            .segments
10274            .iter()
10275            .take(3)
10276            .copied()
10277            .map(Into::into)
10278            .collect::<Vec<ConstraintSegment>>();
10279
10280        assert_eq!(
10281            constraint,
10282            &Constraint::Coincident(Coincident {
10283                segments: expected_segments,
10284            })
10285        );
10286
10287        ctx.close().await;
10288    }
10289
10290    #[tokio::test(flavor = "multi_thread")]
10291    async fn test_point_origin_coincident_preserves_order() {
10292        let initial_source = "\
10293sketch(on = XY) {
10294  point(at = [var 1, var 2])
10295}
10296";
10297
10298        for (origin_first, expected_source) in [
10299            (
10300                true,
10301                "\
10302sketch(on = XY) {
10303  point1 = point(at = [var 0, var 0])
10304  coincident([ORIGIN, point1])
10305}
10306",
10307            ),
10308            (
10309                false,
10310                "\
10311sketch(on = XY) {
10312  point1 = point(at = [var 0, var 0])
10313  coincident([point1, ORIGIN])
10314}
10315",
10316            ),
10317        ] {
10318            let program = Program::parse(initial_source).unwrap().0.unwrap();
10319
10320            let mut frontend = FrontendState::new();
10321
10322            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10323            let mock_ctx = ExecutorContext::new_mock(None).await;
10324            let version = Version(0);
10325
10326            frontend.hack_set_program(&ctx, program).await.unwrap();
10327            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10328            let sketch_id = sketch_object.id;
10329            let sketch = expect_sketch(sketch_object);
10330            let point_id = *sketch.segments.first().unwrap();
10331
10332            let segments = if origin_first {
10333                vec![ConstraintSegment::ORIGIN, point_id.into()]
10334            } else {
10335                vec![point_id.into(), ConstraintSegment::ORIGIN]
10336            };
10337            let constraint = Constraint::Coincident(Coincident {
10338                segments: segments.clone(),
10339            });
10340            let (src_delta, scene_delta) = frontend
10341                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10342                .await
10343                .unwrap();
10344            assert_eq!(src_delta.text.as_str(), expected_source);
10345
10346            let constraint_object = scene_delta
10347                .new_graph
10348                .objects
10349                .iter()
10350                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10351                .unwrap();
10352
10353            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10354                panic!("expected a constraint object");
10355            };
10356
10357            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10358
10359            ctx.close().await;
10360            mock_ctx.close().await;
10361        }
10362    }
10363
10364    #[tokio::test(flavor = "multi_thread")]
10365    async fn test_coincident_of_line_end_points() {
10366        let initial_source = "\
10367sketch(on = XY) {
10368  line(start = [var 1, var 2], end = [var 3, var 4])
10369  line(start = [var 5, var 6], end = [var 7, var 8])
10370}
10371";
10372
10373        let program = Program::parse(initial_source).unwrap().0.unwrap();
10374
10375        let mut frontend = FrontendState::new();
10376
10377        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10378        let mock_ctx = ExecutorContext::new_mock(None).await;
10379        let version = Version(0);
10380
10381        frontend.hack_set_program(&ctx, program).await.unwrap();
10382        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10383        let sketch_id = sketch_object.id;
10384        let sketch = expect_sketch(sketch_object);
10385        let point0_id = *sketch.segments.get(1).unwrap();
10386        let point1_id = *sketch.segments.get(3).unwrap();
10387
10388        let constraint = Constraint::Coincident(Coincident {
10389            segments: vec![point0_id.into(), point1_id.into()],
10390        });
10391        let (src_delta, scene_delta) = frontend
10392            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10393            .await
10394            .unwrap();
10395        assert_eq!(
10396            src_delta.text.as_str(),
10397            "\
10398sketch(on = XY) {
10399  line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10400  line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10401  coincident([line1.end, line2.start])
10402}
10403"
10404        );
10405        assert_eq!(
10406            scene_delta.new_graph.objects.len(),
10407            9,
10408            "{:#?}",
10409            scene_delta.new_graph.objects
10410        );
10411
10412        ctx.close().await;
10413        mock_ctx.close().await;
10414    }
10415
10416    #[tokio::test(flavor = "multi_thread")]
10417    async fn test_coincident_of_line_point_and_circle_segment() {
10418        let initial_source = "\
10419sketch(on = XY) {
10420  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10421  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10422}
10423";
10424        let program = Program::parse(initial_source).unwrap().0.unwrap();
10425        let mut frontend = FrontendState::new();
10426
10427        let mock_ctx = ExecutorContext::new_mock(None).await;
10428        let version = Version(0);
10429
10430        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10431        frontend.program = program;
10432        frontend.update_state_after_exec(outcome, true);
10433        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10434        let sketch_id = sketch_object.id;
10435        let sketch = expect_sketch(sketch_object);
10436
10437        let circle_id = sketch
10438            .segments
10439            .iter()
10440            .copied()
10441            .find(|seg_id| {
10442                matches!(
10443                    &frontend.scene_graph.objects[seg_id.0].kind,
10444                    ObjectKind::Segment {
10445                        segment: Segment::Circle(_)
10446                    }
10447                )
10448            })
10449            .expect("Expected a circle segment in sketch");
10450        let line_id = frontend
10451            .scene_graph
10452            .objects
10453            .iter()
10454            .find_map(|obj| match &obj.kind {
10455                ObjectKind::Segment {
10456                    segment: Segment::Line(line),
10457                } if line.owner.is_none() => Some(obj.id),
10458                _ => None,
10459            })
10460            .expect("Expected a standalone line segment in scene graph");
10461
10462        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10463            ObjectKind::Segment {
10464                segment: Segment::Line(line),
10465            } => line.start,
10466            _ => panic!("Expected line segment object"),
10467        };
10468
10469        let constraint = Constraint::Coincident(Coincident {
10470            segments: vec![line_start_point_id.into(), circle_id.into()],
10471        });
10472        let (src_delta, _scene_delta) = frontend
10473            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10474            .await
10475            .unwrap();
10476        assert_eq!(
10477            src_delta.text.as_str(),
10478            "\
10479sketch(on = XY) {
10480  circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10481  line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10482  coincident([line1.start, circle1])
10483}
10484"
10485        );
10486
10487        mock_ctx.close().await;
10488    }
10489
10490    #[tokio::test(flavor = "multi_thread")]
10491    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10492        // Test that attempting an invalid coincident constraint (arc and line)
10493        // doesn't corrupt the state, allowing subsequent operations to work.
10494        // This test verifies the transactional fix in add_constraint that prevents
10495        // state corruption when invalid constraints are attempted.
10496        // Example: coincident constraint between an arc segment and a straight line segment
10497        // is geometrically invalid and should fail, but state should remain intact.
10498        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10499        let program = Program::empty();
10500
10501        let mut frontend = FrontendState::new();
10502        frontend.program = program;
10503
10504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10505        let mock_ctx = ExecutorContext::new_mock(None).await;
10506        let version = Version(0);
10507
10508        let sketch_args = SketchCtor {
10509            on: Plane::Default(PlaneName::Xy),
10510        };
10511        let (_src_delta, _scene_delta, sketch_id) = frontend
10512            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10513            .await
10514            .unwrap();
10515
10516        // Add an arc segment
10517        let arc_ctor = ArcCtor {
10518            start: Point2d {
10519                x: Expr::Var(Number {
10520                    value: 0.0,
10521                    units: NumericSuffix::Mm,
10522                }),
10523                y: Expr::Var(Number {
10524                    value: 0.0,
10525                    units: NumericSuffix::Mm,
10526                }),
10527            },
10528            end: Point2d {
10529                x: Expr::Var(Number {
10530                    value: 10.0,
10531                    units: NumericSuffix::Mm,
10532                }),
10533                y: Expr::Var(Number {
10534                    value: 10.0,
10535                    units: NumericSuffix::Mm,
10536                }),
10537            },
10538            center: Point2d {
10539                x: Expr::Var(Number {
10540                    value: 10.0,
10541                    units: NumericSuffix::Mm,
10542                }),
10543                y: Expr::Var(Number {
10544                    value: 0.0,
10545                    units: NumericSuffix::Mm,
10546                }),
10547            },
10548            construction: None,
10549        };
10550        let (_src_delta, scene_delta) = frontend
10551            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10552            .await
10553            .unwrap();
10554        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10555        let arc_id = *scene_delta.new_objects.last().unwrap();
10556
10557        // Add a line segment
10558        let line_ctor = LineCtor {
10559            start: Point2d {
10560                x: Expr::Var(Number {
10561                    value: 20.0,
10562                    units: NumericSuffix::Mm,
10563                }),
10564                y: Expr::Var(Number {
10565                    value: 0.0,
10566                    units: NumericSuffix::Mm,
10567                }),
10568            },
10569            end: Point2d {
10570                x: Expr::Var(Number {
10571                    value: 30.0,
10572                    units: NumericSuffix::Mm,
10573                }),
10574                y: Expr::Var(Number {
10575                    value: 10.0,
10576                    units: NumericSuffix::Mm,
10577                }),
10578            },
10579            construction: None,
10580        };
10581        let (_src_delta, scene_delta) = frontend
10582            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10583            .await
10584            .unwrap();
10585        // The line is the last object in new_objects (after the 2 points: start, end)
10586        let line_id = *scene_delta.new_objects.last().unwrap();
10587
10588        // Attempt to add an invalid coincident constraint between arc and line
10589        // This should fail during execution, but state should remain intact
10590        let constraint = Constraint::Coincident(Coincident {
10591            segments: vec![arc_id.into(), line_id.into()],
10592        });
10593        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10594
10595        // The constraint addition should fail (invalid constraint)
10596        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10597
10598        // Verify state is not corrupted by checking that we can still access the scene graph
10599        // and that the original segments are still present with their source ranges
10600        let sketch_object_after =
10601            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10602        let sketch_after = expect_sketch(sketch_object_after);
10603
10604        // Verify both segments are still in the sketch
10605        assert!(
10606            sketch_after.segments.contains(&arc_id),
10607            "Arc segment should still exist after failed constraint"
10608        );
10609        assert!(
10610            sketch_after.segments.contains(&line_id),
10611            "Line segment should still exist after failed constraint"
10612        );
10613
10614        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10615        let arc_obj = frontend
10616            .scene_graph
10617            .objects
10618            .get(arc_id.0)
10619            .expect("Arc object should still be accessible");
10620        let line_obj = frontend
10621            .scene_graph
10622            .objects
10623            .get(line_id.0)
10624            .expect("Line object should still be accessible");
10625
10626        // Verify source ranges are still valid (not corrupted)
10627        // Just verify that the objects are still accessible and have the expected types
10628        match &arc_obj.kind {
10629            ObjectKind::Segment {
10630                segment: Segment::Arc(_),
10631            } => {}
10632            _ => panic!("Arc object should still be an arc segment"),
10633        }
10634        match &line_obj.kind {
10635            ObjectKind::Segment {
10636                segment: Segment::Line(_),
10637            } => {}
10638            _ => panic!("Line object should still be a line segment"),
10639        }
10640
10641        ctx.close().await;
10642        mock_ctx.close().await;
10643    }
10644
10645    #[tokio::test(flavor = "multi_thread")]
10646    async fn test_distance_two_points() {
10647        let initial_source = "\
10648sketch(on = XY) {
10649  point(at = [var 1, var 2])
10650  point(at = [var 3, var 4])
10651}
10652";
10653
10654        let program = Program::parse(initial_source).unwrap().0.unwrap();
10655
10656        let mut frontend = FrontendState::new();
10657
10658        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10659        let mock_ctx = ExecutorContext::new_mock(None).await;
10660        let version = Version(0);
10661
10662        frontend.hack_set_program(&ctx, program).await.unwrap();
10663        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10664        let sketch_id = sketch_object.id;
10665        let sketch = expect_sketch(sketch_object);
10666        let point0_id = *sketch.segments.first().unwrap();
10667        let point1_id = *sketch.segments.get(1).unwrap();
10668
10669        let constraint = Constraint::Distance(Distance {
10670            points: vec![point0_id.into(), point1_id.into()],
10671            distance: Number {
10672                value: 2.0,
10673                units: NumericSuffix::Mm,
10674            },
10675            label_position: None,
10676            source: Default::default(),
10677        });
10678        let (src_delta, scene_delta) = frontend
10679            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10680            .await
10681            .unwrap();
10682        assert_eq!(
10683            src_delta.text.as_str(),
10684            // The lack indentation is a formatter bug.
10685            "\
10686sketch(on = XY) {
10687  point1 = point(at = [var 1.29, var 2.29])
10688  point2 = point(at = [var 2.71, var 3.71])
10689  distance([point1, point2]) == 2mm
10690}
10691"
10692        );
10693        assert_eq!(
10694            scene_delta.new_graph.objects.len(),
10695            5,
10696            "{:#?}",
10697            scene_delta.new_graph.objects
10698        );
10699
10700        ctx.close().await;
10701        mock_ctx.close().await;
10702    }
10703
10704    #[tokio::test(flavor = "multi_thread")]
10705    async fn test_distance_two_points_with_label() {
10706        let initial_source = "\
10707sketch(on = XY) {
10708  point(at = [var 1, var 2])
10709  point(at = [var 3, var 4])
10710}
10711";
10712
10713        let program = Program::parse(initial_source).unwrap().0.unwrap();
10714
10715        let mut frontend = FrontendState::new();
10716
10717        let mock_ctx = ExecutorContext::new_mock(None).await;
10718        let version = Version(0);
10719
10720        frontend.program = program.clone();
10721        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10722        frontend.update_state_after_exec(outcome, true);
10723        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10724        let sketch_id = sketch_object.id;
10725        let sketch = expect_sketch(sketch_object);
10726        let point0_id = *sketch.segments.first().unwrap();
10727        let point1_id = *sketch.segments.get(1).unwrap();
10728
10729        let label_position = Point2d {
10730            x: Number {
10731                value: 10.0,
10732                units: NumericSuffix::Mm,
10733            },
10734            y: Number {
10735                value: 11.0,
10736                units: NumericSuffix::Mm,
10737            },
10738        };
10739        let constraint = Constraint::Distance(Distance {
10740            points: vec![point0_id.into(), point1_id.into()],
10741            distance: Number {
10742                value: 2.0,
10743                units: NumericSuffix::Mm,
10744            },
10745            label_position: Some(label_position.clone()),
10746            source: Default::default(),
10747        });
10748        let (src_delta, scene_delta) = frontend
10749            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10750            .await
10751            .unwrap();
10752        assert_eq!(
10753            src_delta.text.as_str(),
10754            "\
10755sketch(on = XY) {
10756  point1 = point(at = [var 1.29, var 2.29])
10757  point2 = point(at = [var 2.71, var 3.71])
10758  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10759}
10760"
10761        );
10762
10763        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10764        let sketch = expect_sketch(sketch_object);
10765        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10766        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10767            panic!("Expected constraint object");
10768        };
10769        let Constraint::Distance(distance) = constraint else {
10770            panic!("Expected distance constraint");
10771        };
10772        assert_eq!(distance.label_position, Some(label_position));
10773
10774        mock_ctx.close().await;
10775    }
10776
10777    #[tokio::test(flavor = "multi_thread")]
10778    async fn test_edit_distance_constraint_label_position() {
10779        let initial_source = "\
10780sketch(on = XY) {
10781  point(at = [var 1, var 2])
10782  point(at = [var 3, var 2])
10783}
10784";
10785
10786        let program = Program::parse(initial_source).unwrap().0.unwrap();
10787
10788        let mut frontend = FrontendState::new();
10789
10790        let mock_ctx = ExecutorContext::new_mock(None).await;
10791        let version = Version(0);
10792
10793        frontend.program = program.clone();
10794        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10795        frontend.update_state_after_exec(outcome, true);
10796        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10797        let sketch_id = sketch_object.id;
10798        let sketch = expect_sketch(sketch_object);
10799        let point0_id = *sketch.segments.first().unwrap();
10800        let point1_id = *sketch.segments.get(1).unwrap();
10801
10802        let constraint = Constraint::Distance(Distance {
10803            points: vec![point0_id.into(), point1_id.into()],
10804            distance: Number {
10805                value: 2.0,
10806                units: NumericSuffix::Mm,
10807            },
10808            label_position: None,
10809            source: Default::default(),
10810        });
10811        let (_, scene_delta) = frontend
10812            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10813            .await
10814            .unwrap();
10815        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10816        let sketch = expect_sketch(sketch_object);
10817        let constraint_id = sketch.constraints[0];
10818        let label_position = Point2d {
10819            x: Number {
10820                value: 10.0,
10821                units: NumericSuffix::Mm,
10822            },
10823            y: Number {
10824                value: 11.0,
10825                units: NumericSuffix::Mm,
10826            },
10827        };
10828
10829        let (src_delta, scene_delta) = frontend
10830            .edit_distance_constraint_label_position(
10831                &mock_ctx,
10832                version,
10833                sketch_id,
10834                constraint_id,
10835                label_position.clone(),
10836                vec![],
10837            )
10838            .await
10839            .unwrap();
10840        assert_eq!(
10841            src_delta.text.as_str(),
10842            "\
10843sketch(on = XY) {
10844  point1 = point(at = [var 1, var 2])
10845  point2 = point(at = [var 3, var 2])
10846  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10847}
10848"
10849        );
10850
10851        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10852        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10853            panic!("Expected constraint object");
10854        };
10855        let Constraint::Distance(distance) = constraint else {
10856            panic!("Expected distance constraint");
10857        };
10858        assert_eq!(distance.label_position, Some(label_position));
10859
10860        mock_ctx.close().await;
10861    }
10862
10863    #[tokio::test(flavor = "multi_thread")]
10864    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10865        let initial_source = "\
10866sketch(on = XY) {
10867  point1 = point(at = [var 0mm, var 0mm])
10868  point2 = point(at = [var 10mm, var 0mm])
10869  distance([point1, point2]) == 5mm
10870}
10871";
10872
10873        let program = Program::parse(initial_source).unwrap().0.unwrap();
10874        let mut frontend = FrontendState::new();
10875        let mock_ctx = ExecutorContext::new_mock(None).await;
10876        let version = Version(0);
10877
10878        frontend.program = program.clone();
10879        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10880        frontend.update_state_after_exec(outcome, true);
10881        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10882        let sketch_id = sketch_object.id;
10883        let sketch = expect_sketch(sketch_object);
10884        let point0_id = sketch.segments[0];
10885        let point1_id = sketch.segments[1];
10886        let constraint_id = sketch.constraints[0];
10887
10888        let edited_segments = vec![ExistingSegmentCtor {
10889            id: point0_id,
10890            ctor: SegmentCtor::Point(PointCtor {
10891                position: Point2d {
10892                    x: Expr::Var(Number {
10893                        value: 2.0,
10894                        units: NumericSuffix::Mm,
10895                    }),
10896                    y: Expr::Var(Number {
10897                        value: 1.0,
10898                        units: NumericSuffix::Mm,
10899                    }),
10900                },
10901            }),
10902        }];
10903        let (_, scene_delta) = frontend
10904            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10905            .await
10906            .unwrap();
10907        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10908        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10909
10910        let label_position = Point2d {
10911            x: Number {
10912                value: 3.0,
10913                units: NumericSuffix::Mm,
10914            },
10915            y: Number {
10916                value: 4.0,
10917                units: NumericSuffix::Mm,
10918            },
10919        };
10920        let (_, scene_delta) = frontend
10921            .edit_distance_constraint_label_position(
10922                &mock_ctx,
10923                version,
10924                sketch_id,
10925                constraint_id,
10926                label_position,
10927                vec![point0_id],
10928            )
10929            .await
10930            .unwrap();
10931
10932        assert_point_position_close(
10933            point_position(&scene_delta.new_graph, point0_id),
10934            point0_after_segment_edit,
10935        );
10936        assert_point_position_close(
10937            point_position(&scene_delta.new_graph, point1_id),
10938            point1_after_segment_edit,
10939        );
10940
10941        mock_ctx.close().await;
10942    }
10943
10944    #[tokio::test(flavor = "multi_thread")]
10945    async fn test_distance_point_line() {
10946        let initial_source = "\
10947sketch(on = XY) {
10948  point(at = [var 0, var 5])
10949  line(start = [var 0, var 0], end = [var 10, var 0])
10950}
10951";
10952
10953        let program = Program::parse(initial_source).unwrap().0.unwrap();
10954
10955        let mut frontend = FrontendState::new();
10956
10957        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10958        let mock_ctx = ExecutorContext::new_mock(None).await;
10959        let version = Version(0);
10960
10961        frontend.hack_set_program(&ctx, program).await.unwrap();
10962        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10963        let sketch_id = sketch_object.id;
10964        let sketch = expect_sketch(sketch_object);
10965        let point_id = *sketch.segments.first().unwrap();
10966        let line_id = *sketch
10967            .segments
10968            .iter()
10969            .find(|segment_id| {
10970                matches!(
10971                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10972                    Some(ObjectKind::Segment {
10973                        segment: Segment::Line(_)
10974                    })
10975                )
10976            })
10977            .unwrap();
10978
10979        let label_position = Point2d {
10980            x: Number {
10981                value: 10.0,
10982                units: NumericSuffix::Mm,
10983            },
10984            y: Number {
10985                value: 11.0,
10986                units: NumericSuffix::Mm,
10987            },
10988        };
10989        let constraint = Constraint::Distance(Distance {
10990            points: vec![point_id.into(), line_id.into()],
10991            distance: Number {
10992                value: 5.0,
10993                units: NumericSuffix::Mm,
10994            },
10995            label_position: Some(label_position.clone()),
10996            source: Default::default(),
10997        });
10998        let (src_delta, scene_delta) = frontend
10999            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11000            .await
11001            .unwrap();
11002        assert_eq!(
11003            src_delta.text.as_str(),
11004            "\
11005sketch(on = XY) {
11006  point1 = point(at = [var 0, var 5])
11007  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11008  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11009}
11010"
11011        );
11012        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11013        let sketch = expect_sketch(sketch_object);
11014        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11015        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11016            panic!("Expected constraint object");
11017        };
11018        let Constraint::Distance(distance) = constraint else {
11019            panic!("Expected distance constraint");
11020        };
11021        assert_eq!(distance.label_position, Some(label_position));
11022
11023        ctx.close().await;
11024        mock_ctx.close().await;
11025    }
11026
11027    #[tokio::test(flavor = "multi_thread")]
11028    async fn test_distance_point_arc() {
11029        let initial_source = "\
11030sketch(on = XY) {
11031  point(at = [var 0, var 8])
11032  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11033}
11034";
11035
11036        let program = Program::parse(initial_source).unwrap().0.unwrap();
11037
11038        let mut frontend = FrontendState::new();
11039
11040        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11041        let mock_ctx = ExecutorContext::new_mock(None).await;
11042        let version = Version(0);
11043
11044        frontend.hack_set_program(&ctx, program).await.unwrap();
11045        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11046        let sketch_id = sketch_object.id;
11047        let sketch = expect_sketch(sketch_object);
11048        let point_id = *sketch.segments.first().unwrap();
11049        let arc_id = *sketch
11050            .segments
11051            .iter()
11052            .find(|segment_id| {
11053                matches!(
11054                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11055                    Some(ObjectKind::Segment {
11056                        segment: Segment::Arc(_)
11057                    })
11058                )
11059            })
11060            .unwrap();
11061
11062        let constraint = Constraint::Distance(Distance {
11063            points: vec![point_id.into(), arc_id.into()],
11064            distance: Number {
11065                value: 3.0,
11066                units: NumericSuffix::Mm,
11067            },
11068            label_position: None,
11069            source: Default::default(),
11070        });
11071        let (src_delta, _scene_delta) = frontend
11072            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11073            .await
11074            .unwrap();
11075        assert_eq!(
11076            src_delta.text.as_str(),
11077            "\
11078sketch(on = XY) {
11079  point1 = point(at = [var 0, var 8])
11080  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11081  distance([point1, arc1]) == 3mm
11082}
11083"
11084        );
11085
11086        ctx.close().await;
11087        mock_ctx.close().await;
11088    }
11089
11090    #[tokio::test(flavor = "multi_thread")]
11091    async fn test_distance_arc_origin() {
11092        let initial_source = "\
11093sketch001 = sketch(on = XY) {
11094  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11095}
11096";
11097
11098        let program = Program::parse(initial_source).unwrap().0.unwrap();
11099
11100        let mut frontend = FrontendState::new();
11101
11102        let mock_ctx = ExecutorContext::new_mock(None).await;
11103        let version = Version(0);
11104
11105        frontend.program = program.clone();
11106        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11107        frontend.update_state_after_exec(outcome, true);
11108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11109        let sketch_id = sketch_object.id;
11110        let sketch = expect_sketch(sketch_object);
11111        let arc_id = *sketch
11112            .segments
11113            .iter()
11114            .find(|segment_id| {
11115                matches!(
11116                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11117                    Some(ObjectKind::Segment {
11118                        segment: Segment::Arc(_)
11119                    })
11120                )
11121            })
11122            .unwrap();
11123
11124        let constraint = Constraint::Distance(Distance {
11125            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11126            distance: Number {
11127                value: 3.0,
11128                units: NumericSuffix::Mm,
11129            },
11130            label_position: None,
11131            source: Default::default(),
11132        });
11133        let (src_delta, _scene_delta) = frontend
11134            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11135            .await
11136            .unwrap();
11137        assert_eq!(
11138            src_delta.text.as_str(),
11139            "\
11140sketch001 = sketch(on = XY) {
11141  arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11142  distance([arc1, ORIGIN]) == 3mm
11143}
11144"
11145        );
11146
11147        mock_ctx.close().await;
11148    }
11149
11150    #[tokio::test(flavor = "multi_thread")]
11151    async fn test_distance_line_origin() {
11152        let initial_source = "\
11153sketch(on = XY) {
11154  line(start = [var 5, var 0], end = [var 5, var 10])
11155}
11156";
11157
11158        let program = Program::parse(initial_source).unwrap().0.unwrap();
11159
11160        let mut frontend = FrontendState::new();
11161
11162        let mock_ctx = ExecutorContext::new_mock(None).await;
11163        let version = Version(0);
11164
11165        frontend.program = program.clone();
11166        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11167        frontend.update_state_after_exec(outcome, true);
11168        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11169        let sketch_id = sketch_object.id;
11170        let sketch = expect_sketch(sketch_object);
11171        let line_id = *sketch
11172            .segments
11173            .iter()
11174            .find(|segment_id| {
11175                matches!(
11176                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11177                    Some(ObjectKind::Segment {
11178                        segment: Segment::Line(_)
11179                    })
11180                )
11181            })
11182            .unwrap();
11183
11184        let constraint = Constraint::Distance(Distance {
11185            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11186            distance: Number {
11187                value: 5.0,
11188                units: NumericSuffix::Mm,
11189            },
11190            label_position: None,
11191            source: Default::default(),
11192        });
11193        let (src_delta, _scene_delta) = frontend
11194            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11195            .await
11196            .unwrap();
11197        assert_eq!(
11198            src_delta.text.as_str(),
11199            "\
11200sketch(on = XY) {
11201  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11202  distance([ORIGIN, line1]) == 5mm
11203}
11204"
11205        );
11206
11207        mock_ctx.close().await;
11208    }
11209
11210    #[tokio::test(flavor = "multi_thread")]
11211    async fn test_distance_line_circle() {
11212        let initial_source = "\
11213sketch(on = XY) {
11214  line(start = [var -10, var 8], end = [var 10, var 8])
11215  circle(start = [var 5, var 0], center = [var 0, var 0])
11216}
11217";
11218
11219        let program = Program::parse(initial_source).unwrap().0.unwrap();
11220
11221        let mut frontend = FrontendState::new();
11222
11223        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11224        let mock_ctx = ExecutorContext::new_mock(None).await;
11225        let version = Version(0);
11226
11227        frontend.hack_set_program(&ctx, program).await.unwrap();
11228        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11229        let sketch_id = sketch_object.id;
11230        let sketch = expect_sketch(sketch_object);
11231        let line_id = *sketch
11232            .segments
11233            .iter()
11234            .find(|segment_id| {
11235                matches!(
11236                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11237                    Some(ObjectKind::Segment {
11238                        segment: Segment::Line(_)
11239                    })
11240                )
11241            })
11242            .unwrap();
11243        let circle_id = *sketch
11244            .segments
11245            .iter()
11246            .find(|segment_id| {
11247                matches!(
11248                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11249                    Some(ObjectKind::Segment {
11250                        segment: Segment::Circle(_)
11251                    })
11252                )
11253            })
11254            .unwrap();
11255
11256        let constraint = Constraint::Distance(Distance {
11257            points: vec![line_id.into(), circle_id.into()],
11258            distance: Number {
11259                value: 3.0,
11260                units: NumericSuffix::Mm,
11261            },
11262            label_position: None,
11263            source: Default::default(),
11264        });
11265        let (src_delta, _scene_delta) = frontend
11266            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11267            .await
11268            .unwrap();
11269        assert_eq!(
11270            src_delta.text.as_str(),
11271            "\
11272sketch(on = XY) {
11273  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11274  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11275  distance([line1, circle1]) == 3mm
11276}
11277"
11278        );
11279
11280        ctx.close().await;
11281        mock_ctx.close().await;
11282    }
11283
11284    #[tokio::test(flavor = "multi_thread")]
11285    async fn test_distance_circle_arc() {
11286        let initial_source = "\
11287sketch(on = XY) {
11288  circle(start = [var 5, var 0], center = [var 0, var 0])
11289  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11290}
11291";
11292
11293        let program = Program::parse(initial_source).unwrap().0.unwrap();
11294
11295        let mut frontend = FrontendState::new();
11296
11297        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11298        let mock_ctx = ExecutorContext::new_mock(None).await;
11299        let version = Version(0);
11300
11301        frontend.hack_set_program(&ctx, program).await.unwrap();
11302        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11303        let sketch_id = sketch_object.id;
11304        let sketch = expect_sketch(sketch_object);
11305        let circle_id = *sketch
11306            .segments
11307            .iter()
11308            .find(|segment_id| {
11309                matches!(
11310                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11311                    Some(ObjectKind::Segment {
11312                        segment: Segment::Circle(_)
11313                    })
11314                )
11315            })
11316            .unwrap();
11317        let arc_id = *sketch
11318            .segments
11319            .iter()
11320            .find(|segment_id| {
11321                matches!(
11322                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11323                    Some(ObjectKind::Segment {
11324                        segment: Segment::Arc(_)
11325                    })
11326                )
11327            })
11328            .unwrap();
11329
11330        let constraint = Constraint::Distance(Distance {
11331            points: vec![circle_id.into(), arc_id.into()],
11332            distance: Number {
11333                value: 3.0,
11334                units: NumericSuffix::Mm,
11335            },
11336            label_position: None,
11337            source: Default::default(),
11338        });
11339        let (src_delta, _scene_delta) = frontend
11340            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11341            .await
11342            .unwrap();
11343        assert_eq!(
11344            src_delta.text.as_str(),
11345            "\
11346sketch(on = XY) {
11347  circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11348  arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11349  distance([circle1, arc1]) == 3mm
11350}
11351"
11352        );
11353
11354        ctx.close().await;
11355        mock_ctx.close().await;
11356    }
11357
11358    #[tokio::test(flavor = "multi_thread")]
11359    async fn test_distance_parallel_lines() {
11360        let initial_source = "\
11361sketch(on = XY) {
11362  line(start = [var 0, var 0], end = [var 10, var 0])
11363  line(start = [var 0, var 5], end = [var 10, var 5])
11364}
11365";
11366
11367        let program = Program::parse(initial_source).unwrap().0.unwrap();
11368
11369        let mut frontend = FrontendState::new();
11370
11371        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11372        let mock_ctx = ExecutorContext::new_mock(None).await;
11373        let version = Version(0);
11374
11375        frontend.hack_set_program(&ctx, program).await.unwrap();
11376        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11377        let sketch_id = sketch_object.id;
11378        let sketch = expect_sketch(sketch_object);
11379        let line_ids = sketch
11380            .segments
11381            .iter()
11382            .copied()
11383            .filter(|segment_id| {
11384                matches!(
11385                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11386                    Some(ObjectKind::Segment {
11387                        segment: Segment::Line(_)
11388                    })
11389                )
11390            })
11391            .collect::<Vec<_>>();
11392
11393        let constraint = Constraint::Distance(Distance {
11394            points: vec![line_ids[0].into(), line_ids[1].into()],
11395            distance: Number {
11396                value: 5.0,
11397                units: NumericSuffix::Mm,
11398            },
11399            label_position: None,
11400            source: Default::default(),
11401        });
11402        let (src_delta, _scene_delta) = frontend
11403            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11404            .await
11405            .unwrap();
11406        assert_eq!(
11407            src_delta.text.as_str(),
11408            "\
11409sketch(on = XY) {
11410  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11411  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11412  distance([line1, line2]) == 5mm
11413}
11414"
11415        );
11416
11417        ctx.close().await;
11418        mock_ctx.close().await;
11419    }
11420
11421    #[tokio::test(flavor = "multi_thread")]
11422    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11423        let initial_source = "\
11424sketch(on = XY) {
11425  line(start = [var 0, var 0], end = [var 10, var 0])
11426  line(start = [var 0, var 0], end = [var 0, var 10])
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        let line_ids = sketch
11443            .segments
11444            .iter()
11445            .copied()
11446            .filter(|segment_id| {
11447                matches!(
11448                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11449                    Some(ObjectKind::Segment {
11450                        segment: Segment::Line(_)
11451                    })
11452                )
11453            })
11454            .collect::<Vec<_>>();
11455
11456        let constraint = Constraint::Distance(Distance {
11457            points: vec![line_ids[0].into(), line_ids[1].into()],
11458            distance: Number {
11459                value: 5.0,
11460                units: NumericSuffix::Mm,
11461            },
11462            label_position: None,
11463            source: Default::default(),
11464        });
11465        let (src_delta, _scene_delta) = frontend
11466            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11467            .await
11468            .unwrap();
11469        assert_eq!(
11470            src_delta.text.as_str(),
11471            "\
11472sketch(on = XY) {
11473  line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11474  line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11475  distance([line1, line2]) == 5mm
11476}
11477"
11478        );
11479
11480        ctx.close().await;
11481        mock_ctx.close().await;
11482    }
11483
11484    #[tokio::test(flavor = "multi_thread")]
11485    async fn test_horizontal_distance_two_points() {
11486        let initial_source = "\
11487sketch(on = XY) {
11488  point(at = [var 1, var 2])
11489  point(at = [var 3, var 4])
11490}
11491";
11492
11493        let program = Program::parse(initial_source).unwrap().0.unwrap();
11494
11495        let mut frontend = FrontendState::new();
11496
11497        let mock_ctx = ExecutorContext::new_mock(None).await;
11498        let version = Version(0);
11499
11500        frontend.program = program.clone();
11501        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11502        frontend.update_state_after_exec(outcome, true);
11503        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11504        let sketch_id = sketch_object.id;
11505        let sketch = expect_sketch(sketch_object);
11506        let point0_id = *sketch.segments.first().unwrap();
11507        let point1_id = *sketch.segments.get(1).unwrap();
11508        let label_position = Point2d {
11509            x: Number {
11510                value: 10.0,
11511                units: NumericSuffix::Mm,
11512            },
11513            y: Number {
11514                value: 11.0,
11515                units: NumericSuffix::Mm,
11516            },
11517        };
11518
11519        let constraint = Constraint::HorizontalDistance(Distance {
11520            points: vec![point0_id.into(), point1_id.into()],
11521            distance: Number {
11522                value: 2.0,
11523                units: NumericSuffix::Mm,
11524            },
11525            label_position: Some(label_position.clone()),
11526            source: Default::default(),
11527        });
11528        let (src_delta, scene_delta) = frontend
11529            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11530            .await
11531            .unwrap();
11532        assert_eq!(
11533            src_delta.text.as_str(),
11534            // The lack indentation is a formatter bug.
11535            "\
11536sketch(on = XY) {
11537  point1 = point(at = [var 1, var 2])
11538  point2 = point(at = [var 3, var 4])
11539  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11540}
11541"
11542        );
11543        assert_eq!(
11544            scene_delta.new_graph.objects.len(),
11545            5,
11546            "{:#?}",
11547            scene_delta.new_graph.objects
11548        );
11549        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11550        let sketch = expect_sketch(sketch_object);
11551        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11552        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11553            panic!("Expected constraint object");
11554        };
11555        let Constraint::HorizontalDistance(distance) = constraint else {
11556            panic!("Expected horizontal distance constraint");
11557        };
11558        assert_eq!(distance.label_position, Some(label_position));
11559
11560        mock_ctx.close().await;
11561    }
11562
11563    #[tokio::test(flavor = "multi_thread")]
11564    async fn test_radius_single_arc_segment() {
11565        let initial_source = "\
11566sketch(on = XY) {
11567  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11568}
11569";
11570
11571        let program = Program::parse(initial_source).unwrap().0.unwrap();
11572
11573        let mut frontend = FrontendState::new();
11574
11575        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11576        let mock_ctx = ExecutorContext::new_mock(None).await;
11577        let version = Version(0);
11578
11579        frontend.hack_set_program(&ctx, program).await.unwrap();
11580        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11581        let sketch_id = sketch_object.id;
11582        let sketch = expect_sketch(sketch_object);
11583        // Find the arc segment (not the points)
11584        let arc_id = sketch
11585            .segments
11586            .iter()
11587            .find(|&seg_id| {
11588                let obj = frontend.scene_graph.objects.get(seg_id.0);
11589                matches!(
11590                    obj.map(|o| &o.kind),
11591                    Some(ObjectKind::Segment {
11592                        segment: Segment::Arc(_)
11593                    })
11594                )
11595            })
11596            .unwrap();
11597
11598        let constraint = Constraint::Radius(Radius {
11599            arc: *arc_id,
11600            radius: Number {
11601                value: 5.0,
11602                units: NumericSuffix::Mm,
11603            },
11604            label_position: None,
11605            source: Default::default(),
11606        });
11607        let (src_delta, scene_delta) = frontend
11608            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11609            .await
11610            .unwrap();
11611        assert_eq!(
11612            src_delta.text.as_str(),
11613            // The lack indentation is a formatter bug.
11614            "\
11615sketch(on = XY) {
11616  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11617  radius(arc1) == 5mm
11618}
11619"
11620        );
11621        assert_eq!(
11622            scene_delta.new_graph.objects.len(),
11623            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11624            "{:#?}",
11625            scene_delta.new_graph.objects
11626        );
11627
11628        ctx.close().await;
11629        mock_ctx.close().await;
11630    }
11631
11632    #[tokio::test(flavor = "multi_thread")]
11633    async fn test_radius_single_arc_segment_with_label_position() {
11634        let initial_source = "\
11635sketch(on = XY) {
11636  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11637}
11638";
11639
11640        let program = Program::parse(initial_source).unwrap().0.unwrap();
11641        let mut frontend = FrontendState::new();
11642        let mock_ctx = ExecutorContext::new_mock(None).await;
11643        let version = Version(0);
11644
11645        frontend.program = program.clone();
11646        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11647        frontend.update_state_after_exec(outcome, true);
11648        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11649        let sketch_id = sketch_object.id;
11650        let sketch = expect_sketch(sketch_object);
11651        let arc_id = sketch
11652            .segments
11653            .iter()
11654            .find(|&seg_id| {
11655                let obj = frontend.scene_graph.objects.get(seg_id.0);
11656                matches!(
11657                    obj.map(|o| &o.kind),
11658                    Some(ObjectKind::Segment {
11659                        segment: Segment::Arc(_)
11660                    })
11661                )
11662            })
11663            .unwrap();
11664
11665        let label_position = Point2d {
11666            x: Number {
11667                value: 10.0,
11668                units: NumericSuffix::Mm,
11669            },
11670            y: Number {
11671                value: 11.0,
11672                units: NumericSuffix::Mm,
11673            },
11674        };
11675        let constraint = Constraint::Radius(Radius {
11676            arc: *arc_id,
11677            radius: Number {
11678                value: 5.0,
11679                units: NumericSuffix::Mm,
11680            },
11681            label_position: Some(label_position.clone()),
11682            source: Default::default(),
11683        });
11684        let (src_delta, scene_delta) = frontend
11685            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11686            .await
11687            .unwrap();
11688        assert_eq!(
11689            src_delta.text.as_str(),
11690            "\
11691sketch(on = XY) {
11692  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11693  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11694}
11695"
11696        );
11697
11698        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11699        let sketch = expect_sketch(sketch_object);
11700        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11701        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11702            panic!("Expected constraint object");
11703        };
11704        let Constraint::Radius(radius) = constraint else {
11705            panic!("Expected radius constraint");
11706        };
11707        assert_eq!(radius.label_position, Some(label_position));
11708
11709        mock_ctx.close().await;
11710    }
11711
11712    #[tokio::test(flavor = "multi_thread")]
11713    async fn test_edit_radius_constraint_label_position() {
11714        let initial_source = "\
11715sketch(on = XY) {
11716  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11717  radius(arc1) == 5mm
11718}
11719";
11720
11721        let program = Program::parse(initial_source).unwrap().0.unwrap();
11722        let mut frontend = FrontendState::new();
11723        let mock_ctx = ExecutorContext::new_mock(None).await;
11724        let version = Version(0);
11725
11726        frontend.program = program.clone();
11727        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11728        frontend.update_state_after_exec(outcome, true);
11729        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11730        let sketch_id = sketch_object.id;
11731        let sketch = expect_sketch(sketch_object);
11732        let constraint_id = sketch.constraints[0];
11733        let label_position = Point2d {
11734            x: Number {
11735                value: 10.0,
11736                units: NumericSuffix::Mm,
11737            },
11738            y: Number {
11739                value: 11.0,
11740                units: NumericSuffix::Mm,
11741            },
11742        };
11743
11744        let (src_delta, scene_delta) = frontend
11745            .edit_distance_constraint_label_position(
11746                &mock_ctx,
11747                version,
11748                sketch_id,
11749                constraint_id,
11750                label_position.clone(),
11751                vec![],
11752            )
11753            .await
11754            .unwrap();
11755        assert_eq!(
11756            src_delta.text.as_str(),
11757            "\
11758sketch(on = XY) {
11759  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11760  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11761}
11762"
11763        );
11764
11765        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11766        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11767            panic!("Expected constraint object");
11768        };
11769        let Constraint::Radius(radius) = constraint else {
11770            panic!("Expected radius constraint");
11771        };
11772        assert_eq!(radius.label_position, Some(label_position));
11773
11774        mock_ctx.close().await;
11775    }
11776
11777    #[tokio::test(flavor = "multi_thread")]
11778    async fn test_vertical_distance_two_points() {
11779        let initial_source = "\
11780sketch(on = XY) {
11781  point(at = [var 1, var 2])
11782  point(at = [var 3, var 4])
11783}
11784";
11785
11786        let program = Program::parse(initial_source).unwrap().0.unwrap();
11787
11788        let mut frontend = FrontendState::new();
11789
11790        let mock_ctx = ExecutorContext::new_mock(None).await;
11791        let version = Version(0);
11792
11793        frontend.program = program.clone();
11794        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11795        frontend.update_state_after_exec(outcome, true);
11796        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11797        let sketch_id = sketch_object.id;
11798        let sketch = expect_sketch(sketch_object);
11799        let point0_id = *sketch.segments.first().unwrap();
11800        let point1_id = *sketch.segments.get(1).unwrap();
11801        let label_position = Point2d {
11802            x: Number {
11803                value: 10.0,
11804                units: NumericSuffix::Mm,
11805            },
11806            y: Number {
11807                value: 11.0,
11808                units: NumericSuffix::Mm,
11809            },
11810        };
11811
11812        let constraint = Constraint::VerticalDistance(Distance {
11813            points: vec![point0_id.into(), point1_id.into()],
11814            distance: Number {
11815                value: 2.0,
11816                units: NumericSuffix::Mm,
11817            },
11818            label_position: Some(label_position.clone()),
11819            source: Default::default(),
11820        });
11821        let (src_delta, scene_delta) = frontend
11822            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11823            .await
11824            .unwrap();
11825        assert_eq!(
11826            src_delta.text.as_str(),
11827            // The lack indentation is a formatter bug.
11828            "\
11829sketch(on = XY) {
11830  point1 = point(at = [var 1, var 2])
11831  point2 = point(at = [var 3, var 4])
11832  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11833}
11834"
11835        );
11836        assert_eq!(
11837            scene_delta.new_graph.objects.len(),
11838            5,
11839            "{:#?}",
11840            scene_delta.new_graph.objects
11841        );
11842        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11843        let sketch = expect_sketch(sketch_object);
11844        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11845        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11846            panic!("Expected constraint object");
11847        };
11848        let Constraint::VerticalDistance(distance) = constraint else {
11849            panic!("Expected vertical distance constraint");
11850        };
11851        assert_eq!(distance.label_position, Some(label_position));
11852
11853        mock_ctx.close().await;
11854    }
11855
11856    #[tokio::test(flavor = "multi_thread")]
11857    async fn test_add_fixed_standalone_point() {
11858        let initial_source = "\
11859sketch(on = XY) {
11860  point(at = [var 1, var 2])
11861}
11862";
11863
11864        let program = Program::parse(initial_source).unwrap().0.unwrap();
11865
11866        let mut frontend = FrontendState::new();
11867
11868        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11869        let mock_ctx = ExecutorContext::new_mock(None).await;
11870        let version = Version(0);
11871
11872        frontend.hack_set_program(&ctx, program).await.unwrap();
11873        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11874        let sketch_id = sketch_object.id;
11875        let sketch = expect_sketch(sketch_object);
11876        let point_id = *sketch.segments.first().unwrap();
11877
11878        let (src_delta, scene_delta) = frontend
11879            .add_constraint(
11880                &mock_ctx,
11881                version,
11882                sketch_id,
11883                Constraint::Fixed(Fixed {
11884                    points: vec![FixedPoint {
11885                        point: point_id,
11886                        position: Point2d {
11887                            x: Number {
11888                                value: 2.0,
11889                                units: NumericSuffix::Mm,
11890                            },
11891                            y: Number {
11892                                value: 3.0,
11893                                units: NumericSuffix::Mm,
11894                            },
11895                        },
11896                    }],
11897                }),
11898            )
11899            .await
11900            .unwrap();
11901        assert_eq!(
11902            src_delta.text.as_str(),
11903            "\
11904sketch(on = XY) {
11905  point1 = point(at = [var 2, var 3])
11906  fixed([point1, [2mm, 3mm]])
11907}
11908"
11909        );
11910        assert_eq!(
11911            scene_delta.new_graph.objects.len(),
11912            4,
11913            "{:#?}",
11914            scene_delta.new_graph.objects
11915        );
11916
11917        ctx.close().await;
11918        mock_ctx.close().await;
11919    }
11920
11921    #[tokio::test(flavor = "multi_thread")]
11922    async fn test_add_fixed_multiple_points() {
11923        let initial_source = "\
11924sketch(on = XY) {
11925  point(at = [var 1, var 2])
11926  point(at = [var 3, var 4])
11927}
11928";
11929
11930        let program = Program::parse(initial_source).unwrap().0.unwrap();
11931
11932        let mut frontend = FrontendState::new();
11933
11934        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11935        let mock_ctx = ExecutorContext::new_mock(None).await;
11936        let version = Version(0);
11937
11938        frontend.hack_set_program(&ctx, program).await.unwrap();
11939        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11940        let sketch_id = sketch_object.id;
11941        let sketch = expect_sketch(sketch_object);
11942        let point0_id = *sketch.segments.first().unwrap();
11943        let point1_id = *sketch.segments.get(1).unwrap();
11944
11945        let (src_delta, scene_delta) = frontend
11946            .add_constraint(
11947                &mock_ctx,
11948                version,
11949                sketch_id,
11950                Constraint::Fixed(Fixed {
11951                    points: vec![
11952                        FixedPoint {
11953                            point: point0_id,
11954                            position: Point2d {
11955                                x: Number {
11956                                    value: 2.0,
11957                                    units: NumericSuffix::Mm,
11958                                },
11959                                y: Number {
11960                                    value: 3.0,
11961                                    units: NumericSuffix::Mm,
11962                                },
11963                            },
11964                        },
11965                        FixedPoint {
11966                            point: point1_id,
11967                            position: Point2d {
11968                                x: Number {
11969                                    value: 4.0,
11970                                    units: NumericSuffix::Mm,
11971                                },
11972                                y: Number {
11973                                    value: 5.0,
11974                                    units: NumericSuffix::Mm,
11975                                },
11976                            },
11977                        },
11978                    ],
11979                }),
11980            )
11981            .await
11982            .unwrap();
11983        assert_eq!(
11984            src_delta.text.as_str(),
11985            "\
11986sketch(on = XY) {
11987  point1 = point(at = [var 2, var 3])
11988  point2 = point(at = [var 4, var 5])
11989  fixed([point1, [2mm, 3mm]])
11990  fixed([point2, [4mm, 5mm]])
11991}
11992"
11993        );
11994        assert_eq!(
11995            scene_delta.new_graph.objects.len(),
11996            6,
11997            "{:#?}",
11998            scene_delta.new_graph.objects
11999        );
12000
12001        ctx.close().await;
12002        mock_ctx.close().await;
12003    }
12004
12005    #[tokio::test(flavor = "multi_thread")]
12006    async fn test_add_fixed_owned_point() {
12007        let initial_source = "\
12008sketch(on = XY) {
12009  line(start = [var 1, var 2], end = [var 3, var 4])
12010}
12011";
12012
12013        let program = Program::parse(initial_source).unwrap().0.unwrap();
12014
12015        let mut frontend = FrontendState::new();
12016
12017        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12018        let mock_ctx = ExecutorContext::new_mock(None).await;
12019        let version = Version(0);
12020
12021        frontend.hack_set_program(&ctx, program).await.unwrap();
12022        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12023        let sketch_id = sketch_object.id;
12024        let sketch = expect_sketch(sketch_object);
12025        let line_start_id = *sketch.segments.first().unwrap();
12026
12027        let (src_delta, scene_delta) = frontend
12028            .add_constraint(
12029                &mock_ctx,
12030                version,
12031                sketch_id,
12032                Constraint::Fixed(Fixed {
12033                    points: vec![FixedPoint {
12034                        point: line_start_id,
12035                        position: Point2d {
12036                            x: Number {
12037                                value: 2.0,
12038                                units: NumericSuffix::Mm,
12039                            },
12040                            y: Number {
12041                                value: 3.0,
12042                                units: NumericSuffix::Mm,
12043                            },
12044                        },
12045                    }],
12046                }),
12047            )
12048            .await
12049            .unwrap();
12050        assert_eq!(
12051            src_delta.text.as_str(),
12052            "\
12053sketch(on = XY) {
12054  line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12055  fixed([line1.start, [2mm, 3mm]])
12056}
12057"
12058        );
12059        assert_eq!(
12060            scene_delta.new_graph.objects.len(),
12061            6,
12062            "{:#?}",
12063            scene_delta.new_graph.objects
12064        );
12065
12066        ctx.close().await;
12067        mock_ctx.close().await;
12068    }
12069
12070    #[tokio::test(flavor = "multi_thread")]
12071    async fn test_radius_error_cases() {
12072        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12073        let mock_ctx = ExecutorContext::new_mock(None).await;
12074        let version = Version(0);
12075
12076        // Test: Single point should error
12077        let initial_source_point = "\
12078sketch(on = XY) {
12079  point(at = [var 1, var 2])
12080}
12081";
12082        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12083        let mut frontend_point = FrontendState::new();
12084        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12085        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12086        let sketch_id_point = sketch_object_point.id;
12087        let sketch_point = expect_sketch(sketch_object_point);
12088        let point_id = *sketch_point.segments.first().unwrap();
12089
12090        let constraint_point = Constraint::Radius(Radius {
12091            arc: point_id,
12092            radius: Number {
12093                value: 5.0,
12094                units: NumericSuffix::Mm,
12095            },
12096            label_position: None,
12097            source: Default::default(),
12098        });
12099        let result_point = frontend_point
12100            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12101            .await;
12102        assert!(result_point.is_err(), "Single point should error for radius");
12103
12104        // Test: Single line segment should error (only arc segments supported)
12105        let initial_source_line = "\
12106sketch(on = XY) {
12107  line(start = [var 1, var 2], end = [var 3, var 4])
12108}
12109";
12110        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12111        let mut frontend_line = FrontendState::new();
12112        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12113        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12114        let sketch_id_line = sketch_object_line.id;
12115        let sketch_line = expect_sketch(sketch_object_line);
12116        let line_id = *sketch_line.segments.first().unwrap();
12117
12118        let constraint_line = Constraint::Radius(Radius {
12119            arc: line_id,
12120            radius: Number {
12121                value: 5.0,
12122                units: NumericSuffix::Mm,
12123            },
12124            label_position: None,
12125            source: Default::default(),
12126        });
12127        let result_line = frontend_line
12128            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12129            .await;
12130        assert!(result_line.is_err(), "Single line segment should error for radius");
12131
12132        ctx.close().await;
12133        mock_ctx.close().await;
12134    }
12135
12136    #[tokio::test(flavor = "multi_thread")]
12137    async fn test_diameter_single_arc_segment() {
12138        let initial_source = "\
12139sketch(on = XY) {
12140  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12141}
12142";
12143
12144        let program = Program::parse(initial_source).unwrap().0.unwrap();
12145
12146        let mut frontend = FrontendState::new();
12147
12148        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12149        let mock_ctx = ExecutorContext::new_mock(None).await;
12150        let version = Version(0);
12151
12152        frontend.hack_set_program(&ctx, program).await.unwrap();
12153        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12154        let sketch_id = sketch_object.id;
12155        let sketch = expect_sketch(sketch_object);
12156        // Find the arc segment (not the points)
12157        let arc_id = sketch
12158            .segments
12159            .iter()
12160            .find(|&seg_id| {
12161                let obj = frontend.scene_graph.objects.get(seg_id.0);
12162                matches!(
12163                    obj.map(|o| &o.kind),
12164                    Some(ObjectKind::Segment {
12165                        segment: Segment::Arc(_)
12166                    })
12167                )
12168            })
12169            .unwrap();
12170
12171        let constraint = Constraint::Diameter(Diameter {
12172            arc: *arc_id,
12173            diameter: Number {
12174                value: 10.0,
12175                units: NumericSuffix::Mm,
12176            },
12177            label_position: None,
12178            source: Default::default(),
12179        });
12180        let (src_delta, scene_delta) = frontend
12181            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12182            .await
12183            .unwrap();
12184        assert_eq!(
12185            src_delta.text.as_str(),
12186            // The lack indentation is a formatter bug.
12187            "\
12188sketch(on = XY) {
12189  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12190  diameter(arc1) == 10mm
12191}
12192"
12193        );
12194        assert_eq!(
12195            scene_delta.new_graph.objects.len(),
12196            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12197            "{:#?}",
12198            scene_delta.new_graph.objects
12199        );
12200
12201        ctx.close().await;
12202        mock_ctx.close().await;
12203    }
12204
12205    #[tokio::test(flavor = "multi_thread")]
12206    async fn test_diameter_single_arc_segment_with_label_position() {
12207        let initial_source = "\
12208sketch(on = XY) {
12209  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12210}
12211";
12212
12213        let program = Program::parse(initial_source).unwrap().0.unwrap();
12214        let mut frontend = FrontendState::new();
12215        let mock_ctx = ExecutorContext::new_mock(None).await;
12216        let version = Version(0);
12217
12218        frontend.program = program.clone();
12219        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12220        frontend.update_state_after_exec(outcome, true);
12221        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12222        let sketch_id = sketch_object.id;
12223        let sketch = expect_sketch(sketch_object);
12224        let arc_id = sketch
12225            .segments
12226            .iter()
12227            .find(|&seg_id| {
12228                let obj = frontend.scene_graph.objects.get(seg_id.0);
12229                matches!(
12230                    obj.map(|o| &o.kind),
12231                    Some(ObjectKind::Segment {
12232                        segment: Segment::Arc(_)
12233                    })
12234                )
12235            })
12236            .unwrap();
12237
12238        let label_position = Point2d {
12239            x: Number {
12240                value: 10.0,
12241                units: NumericSuffix::Mm,
12242            },
12243            y: Number {
12244                value: 11.0,
12245                units: NumericSuffix::Mm,
12246            },
12247        };
12248        let constraint = Constraint::Diameter(Diameter {
12249            arc: *arc_id,
12250            diameter: Number {
12251                value: 10.0,
12252                units: NumericSuffix::Mm,
12253            },
12254            label_position: Some(label_position.clone()),
12255            source: Default::default(),
12256        });
12257        let (src_delta, scene_delta) = frontend
12258            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12259            .await
12260            .unwrap();
12261        assert_eq!(
12262            src_delta.text.as_str(),
12263            "\
12264sketch(on = XY) {
12265  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12266  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12267}
12268"
12269        );
12270
12271        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12272        let sketch = expect_sketch(sketch_object);
12273        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12274        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12275            panic!("Expected constraint object");
12276        };
12277        let Constraint::Diameter(diameter) = constraint else {
12278            panic!("Expected diameter constraint");
12279        };
12280        assert_eq!(diameter.label_position, Some(label_position));
12281
12282        mock_ctx.close().await;
12283    }
12284
12285    #[tokio::test(flavor = "multi_thread")]
12286    async fn test_edit_diameter_constraint_label_position() {
12287        let initial_source = "\
12288sketch(on = XY) {
12289  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12290  diameter(arc1) == 10mm
12291}
12292";
12293
12294        let program = Program::parse(initial_source).unwrap().0.unwrap();
12295        let mut frontend = FrontendState::new();
12296        let mock_ctx = ExecutorContext::new_mock(None).await;
12297        let version = Version(0);
12298
12299        frontend.program = program.clone();
12300        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12301        frontend.update_state_after_exec(outcome, true);
12302        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12303        let sketch_id = sketch_object.id;
12304        let sketch = expect_sketch(sketch_object);
12305        let constraint_id = sketch.constraints[0];
12306        let label_position = Point2d {
12307            x: Number {
12308                value: 10.0,
12309                units: NumericSuffix::Mm,
12310            },
12311            y: Number {
12312                value: 11.0,
12313                units: NumericSuffix::Mm,
12314            },
12315        };
12316
12317        let (src_delta, scene_delta) = frontend
12318            .edit_distance_constraint_label_position(
12319                &mock_ctx,
12320                version,
12321                sketch_id,
12322                constraint_id,
12323                label_position.clone(),
12324                vec![],
12325            )
12326            .await
12327            .unwrap();
12328        assert_eq!(
12329            src_delta.text.as_str(),
12330            "\
12331sketch(on = XY) {
12332  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12333  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12334}
12335"
12336        );
12337
12338        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12339        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12340            panic!("Expected constraint object");
12341        };
12342        let Constraint::Diameter(diameter) = constraint else {
12343            panic!("Expected diameter constraint");
12344        };
12345        assert_eq!(diameter.label_position, Some(label_position));
12346
12347        mock_ctx.close().await;
12348    }
12349
12350    #[tokio::test(flavor = "multi_thread")]
12351    async fn test_diameter_error_cases() {
12352        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12353        let mock_ctx = ExecutorContext::new_mock(None).await;
12354        let version = Version(0);
12355
12356        // Test: Single point should error
12357        let initial_source_point = "\
12358sketch(on = XY) {
12359  point(at = [var 1, var 2])
12360}
12361";
12362        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12363        let mut frontend_point = FrontendState::new();
12364        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12365        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12366        let sketch_id_point = sketch_object_point.id;
12367        let sketch_point = expect_sketch(sketch_object_point);
12368        let point_id = *sketch_point.segments.first().unwrap();
12369
12370        let constraint_point = Constraint::Diameter(Diameter {
12371            arc: point_id,
12372            diameter: Number {
12373                value: 10.0,
12374                units: NumericSuffix::Mm,
12375            },
12376            label_position: None,
12377            source: Default::default(),
12378        });
12379        let result_point = frontend_point
12380            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12381            .await;
12382        assert!(result_point.is_err(), "Single point should error for diameter");
12383
12384        // Test: Single line segment should error (only arc segments supported)
12385        let initial_source_line = "\
12386sketch(on = XY) {
12387  line(start = [var 1, var 2], end = [var 3, var 4])
12388}
12389";
12390        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12391        let mut frontend_line = FrontendState::new();
12392        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12393        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12394        let sketch_id_line = sketch_object_line.id;
12395        let sketch_line = expect_sketch(sketch_object_line);
12396        let line_id = *sketch_line.segments.first().unwrap();
12397
12398        let constraint_line = Constraint::Diameter(Diameter {
12399            arc: line_id,
12400            diameter: Number {
12401                value: 10.0,
12402                units: NumericSuffix::Mm,
12403            },
12404            label_position: None,
12405            source: Default::default(),
12406        });
12407        let result_line = frontend_line
12408            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12409            .await;
12410        assert!(result_line.is_err(), "Single line segment should error for diameter");
12411
12412        ctx.close().await;
12413        mock_ctx.close().await;
12414    }
12415
12416    #[tokio::test(flavor = "multi_thread")]
12417    async fn test_line_horizontal() {
12418        let initial_source = "\
12419sketch(on = XY) {
12420  line(start = [var 1, var 2], end = [var 3, var 4])
12421}
12422";
12423
12424        let program = Program::parse(initial_source).unwrap().0.unwrap();
12425
12426        let mut frontend = FrontendState::new();
12427
12428        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12429        let mock_ctx = ExecutorContext::new_mock(None).await;
12430        let version = Version(0);
12431
12432        frontend.hack_set_program(&ctx, program).await.unwrap();
12433        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12434        let sketch_id = sketch_object.id;
12435        let sketch = expect_sketch(sketch_object);
12436        let line1_id = *sketch.segments.get(2).unwrap();
12437
12438        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12439        let (src_delta, scene_delta) = frontend
12440            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12441            .await
12442            .unwrap();
12443        assert_eq!(
12444            src_delta.text.as_str(),
12445            "\
12446sketch(on = XY) {
12447  line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12448  horizontal(line1)
12449}
12450"
12451        );
12452        assert_eq!(
12453            scene_delta.new_graph.objects.len(),
12454            6,
12455            "{:#?}",
12456            scene_delta.new_graph.objects
12457        );
12458
12459        ctx.close().await;
12460        mock_ctx.close().await;
12461    }
12462
12463    #[tokio::test(flavor = "multi_thread")]
12464    async fn test_control_point_spline_edge_horizontal() {
12465        let initial_source = "\
12466@settings(experimentalFeatures = allow)
12467splineSketch = sketch(on = XY) {
12468  controlPointSpline1 = controlPointSpline(points = [
12469    [var 0mm, var 0mm],
12470    [var 10mm, var 20mm],
12471    [var 20mm, var 0mm],
12472  ])
12473}
12474";
12475
12476        let program = Program::parse(initial_source).unwrap().0.unwrap();
12477
12478        let mut frontend = FrontendState::new();
12479
12480        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12481        let mock_ctx = ExecutorContext::new_mock(None).await;
12482        let version = Version(0);
12483
12484        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12485        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12486        let sketch_id = sketch_object.id;
12487        let sketch = expect_sketch(sketch_object);
12488        let spline_id = sketch
12489            .segments
12490            .iter()
12491            .copied()
12492            .find(|seg_id| {
12493                matches!(
12494                    &frontend.scene_graph.objects[seg_id.0].kind,
12495                    ObjectKind::Segment {
12496                        segment: Segment::ControlPointSpline(_)
12497                    }
12498                )
12499            })
12500            .expect("Expected a control point spline segment in sketch");
12501        let edge_id = frontend
12502            .scene_graph
12503            .objects
12504            .iter()
12505            .find_map(|obj| match &obj.kind {
12506                ObjectKind::Segment {
12507                    segment: Segment::Line(line),
12508                } if line.owner == Some(spline_id) => Some(obj.id),
12509                _ => None,
12510            })
12511            .expect("Expected an owned control-polygon edge");
12512
12513        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12514        let (src_delta, _) = frontend
12515            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12516            .await
12517            .unwrap();
12518        assert!(
12519            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12520            "Expected horizontal constraint on spline edge, got: {}",
12521            src_delta.text
12522        );
12523
12524        ctx.close().await;
12525        mock_ctx.close().await;
12526    }
12527
12528    #[tokio::test(flavor = "multi_thread")]
12529    async fn test_control_point_spline_edge_angle() {
12530        let initial_source = "\
12531@settings(experimentalFeatures = allow)
12532splineSketch = sketch(on = XY) {
12533  controlPointSpline1 = controlPointSpline(points = [
12534    [var 0mm, var 0mm],
12535    [var 10mm, var 20mm],
12536    [var 20mm, var 0mm],
12537  ])
12538
12539  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12540}
12541";
12542
12543        let program = Program::parse(initial_source).unwrap().0.unwrap();
12544
12545        let mut frontend = FrontendState::new();
12546
12547        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12548        let mock_ctx = ExecutorContext::new_mock(None).await;
12549        let version = Version(0);
12550
12551        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12552        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12553        let sketch_id = sketch_object.id;
12554        let sketch = expect_sketch(sketch_object);
12555        let spline_id = sketch
12556            .segments
12557            .iter()
12558            .copied()
12559            .find(|seg_id| {
12560                matches!(
12561                    &frontend.scene_graph.objects[seg_id.0].kind,
12562                    ObjectKind::Segment {
12563                        segment: Segment::ControlPointSpline(_)
12564                    }
12565                )
12566            })
12567            .expect("Expected a control point spline segment in sketch");
12568        let edge_id = frontend
12569            .scene_graph
12570            .objects
12571            .iter()
12572            .find_map(|obj| match &obj.kind {
12573                ObjectKind::Segment {
12574                    segment: Segment::Line(line),
12575                } if line.owner == Some(spline_id) => Some(obj.id),
12576                _ => None,
12577            })
12578            .expect("Expected an owned control-polygon edge");
12579        let line1_id = frontend
12580            .scene_graph
12581            .objects
12582            .iter()
12583            .find_map(|obj| match &obj.kind {
12584                ObjectKind::Segment {
12585                    segment: Segment::Line(line),
12586                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12587                _ => None,
12588            })
12589            .or_else(|| {
12590                sketch.segments.iter().copied().find(|seg_id| {
12591                    matches!(
12592                        &frontend.scene_graph.objects[seg_id.0].kind,
12593                        ObjectKind::Segment {
12594                            segment: Segment::Line(line),
12595                        } if line.owner.is_none()
12596                    )
12597                })
12598            })
12599            .expect("Expected a standalone line segment in sketch");
12600
12601        let constraint = Constraint::Angle(Angle {
12602            lines: vec![line1_id, edge_id],
12603            angle: Number {
12604                value: 30.0,
12605                units: NumericSuffix::Deg,
12606            },
12607            source: Default::default(),
12608        });
12609        let (src_delta, _) = frontend
12610            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12611            .await
12612            .unwrap();
12613        assert!(
12614            src_delta
12615                .text
12616                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12617            "Expected angle constraint on spline edge, got: {}",
12618            src_delta.text
12619        );
12620
12621        ctx.close().await;
12622        mock_ctx.close().await;
12623    }
12624
12625    #[tokio::test(flavor = "multi_thread")]
12626    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12627        let initial_source = "\
12628@settings(experimentalFeatures = allow)
12629splineSketch = sketch(on = XY) {
12630  spline1 = controlPointSpline(points = [
12631    [var 0mm, var 0mm],
12632    [var 10mm, var 20mm],
12633    [var 20mm, var 0mm],
12634  ])
12635  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12636  coincident([spline1.controls[1], spline1.edges[0]])
12637  coincident([spline1.controls[0], line1])
12638}
12639";
12640
12641        let program = Program::parse(initial_source).unwrap().0.unwrap();
12642
12643        let mut frontend = FrontendState::new();
12644
12645        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12646        let mock_ctx = ExecutorContext::new_mock(None).await;
12647
12648        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12649
12650        let ui_scene_graph = frontend.scene_graph_for_ui();
12651        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12652        let sketch = expect_sketch(sketch_object);
12653
12654        assert_eq!(
12655            sketch.constraints.len(),
12656            1,
12657            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12658        );
12659
12660        let visible_constraints = ui_scene_graph
12661            .objects
12662            .iter()
12663            .filter_map(|object| match &object.kind {
12664                ObjectKind::Constraint {
12665                    constraint: Constraint::Coincident(coincident),
12666                } => Some(coincident.clone()),
12667                _ => None,
12668            })
12669            .collect::<Vec<_>>();
12670
12671        assert_eq!(
12672            visible_constraints.len(),
12673            1,
12674            "Expected only one coincident constraint object in the UI scene graph"
12675        );
12676        assert_eq!(
12677            visible_constraints[0].get_segments().len(),
12678            2,
12679            "Expected the remaining visible coincident constraint to reference two segments"
12680        );
12681
12682        ctx.close().await;
12683        mock_ctx.close().await;
12684    }
12685
12686    #[tokio::test(flavor = "multi_thread")]
12687    async fn test_edit_control_point_spline_can_append_control_point() {
12688        let initial_source = "\
12689@settings(experimentalFeatures = allow)
12690splineSketch = sketch(on = XY) {
12691  controlPointSpline(points = [
12692    [var 0mm, var 0mm],
12693    [var 10mm, var 20mm],
12694    [var 20mm, var 0mm],
12695  ])
12696}
12697";
12698
12699        let program = Program::parse(initial_source).unwrap().0.unwrap();
12700
12701        let mut frontend = FrontendState::new();
12702
12703        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12704        let mock_ctx = ExecutorContext::new_mock(None).await;
12705        let version = Version(0);
12706
12707        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12708        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12709        let sketch_id = sketch_object.id;
12710        let sketch = expect_sketch(sketch_object);
12711        let spline_id = sketch
12712            .segments
12713            .iter()
12714            .copied()
12715            .find(|seg_id| {
12716                matches!(
12717                    &frontend.scene_graph.objects[seg_id.0].kind,
12718                    ObjectKind::Segment {
12719                        segment: Segment::ControlPointSpline(_)
12720                    }
12721                )
12722            })
12723            .expect("Expected a control point spline segment in sketch");
12724
12725        let ctor = ControlPointSplineCtor {
12726            points: vec![
12727                Point2d {
12728                    x: Expr::Var(Number {
12729                        value: 0.0,
12730                        units: NumericSuffix::Mm,
12731                    }),
12732                    y: Expr::Var(Number {
12733                        value: 0.0,
12734                        units: NumericSuffix::Mm,
12735                    }),
12736                },
12737                Point2d {
12738                    x: Expr::Var(Number {
12739                        value: 10.0,
12740                        units: NumericSuffix::Mm,
12741                    }),
12742                    y: Expr::Var(Number {
12743                        value: 20.0,
12744                        units: NumericSuffix::Mm,
12745                    }),
12746                },
12747                Point2d {
12748                    x: Expr::Var(Number {
12749                        value: 20.0,
12750                        units: NumericSuffix::Mm,
12751                    }),
12752                    y: Expr::Var(Number {
12753                        value: 0.0,
12754                        units: NumericSuffix::Mm,
12755                    }),
12756                },
12757                Point2d {
12758                    x: Expr::Var(Number {
12759                        value: 30.0,
12760                        units: NumericSuffix::Mm,
12761                    }),
12762                    y: Expr::Var(Number {
12763                        value: 10.0,
12764                        units: NumericSuffix::Mm,
12765                    }),
12766                },
12767            ],
12768            construction: None,
12769        };
12770
12771        let segments = vec![ExistingSegmentCtor {
12772            id: spline_id,
12773            ctor: SegmentCtor::ControlPointSpline(ctor),
12774        }];
12775        let (src_delta, scene_delta) = frontend
12776            .edit_segments(&mock_ctx, version, sketch_id, segments)
12777            .await
12778            .unwrap();
12779
12780        assert!(
12781            src_delta.text.contains("[var 30mm, var 10mm]"),
12782            "Expected appended spline control point in source, got: {}",
12783            src_delta.text
12784        );
12785
12786        assert!(
12787            scene_delta.invalidates_ids,
12788            "Expected appending a spline control point to invalidate ids"
12789        );
12790        let updated_spline = scene_delta
12791            .new_graph
12792            .objects
12793            .iter()
12794            .find_map(|obj| match &obj.kind {
12795                ObjectKind::Segment {
12796                    segment: Segment::ControlPointSpline(updated_spline),
12797                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12798                _ => None,
12799            })
12800            .expect("Expected edited scene graph to contain a four-point control point spline");
12801        assert_eq!(
12802            updated_spline.controls.len(),
12803            4,
12804            "Expected edited spline to expose four control points"
12805        );
12806
12807        ctx.close().await;
12808        mock_ctx.close().await;
12809    }
12810
12811    #[tokio::test(flavor = "multi_thread")]
12812    async fn test_line_vertical() {
12813        let initial_source = "\
12814sketch(on = XY) {
12815  line(start = [var 1, var 2], end = [var 3, var 4])
12816}
12817";
12818
12819        let program = Program::parse(initial_source).unwrap().0.unwrap();
12820
12821        let mut frontend = FrontendState::new();
12822
12823        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12824        let mock_ctx = ExecutorContext::new_mock(None).await;
12825        let version = Version(0);
12826
12827        frontend.hack_set_program(&ctx, program).await.unwrap();
12828        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12829        let sketch_id = sketch_object.id;
12830        let sketch = expect_sketch(sketch_object);
12831        let line1_id = *sketch.segments.get(2).unwrap();
12832
12833        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12834        let (src_delta, scene_delta) = frontend
12835            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12836            .await
12837            .unwrap();
12838        assert_eq!(
12839            src_delta.text.as_str(),
12840            "\
12841sketch(on = XY) {
12842  line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12843  vertical(line1)
12844}
12845"
12846        );
12847        assert_eq!(
12848            scene_delta.new_graph.objects.len(),
12849            6,
12850            "{:#?}",
12851            scene_delta.new_graph.objects
12852        );
12853
12854        ctx.close().await;
12855        mock_ctx.close().await;
12856    }
12857
12858    #[tokio::test(flavor = "multi_thread")]
12859    async fn test_points_vertical() {
12860        let initial_source = "\
12861sketch001 = sketch(on = XY) {
12862  p0 = point(at = [var -2.23mm, var 3.1mm])
12863  pf = point(at = [4, 4])
12864}
12865";
12866
12867        let program = Program::parse(initial_source).unwrap().0.unwrap();
12868
12869        let mut frontend = FrontendState::new();
12870
12871        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12872        let mock_ctx = ExecutorContext::new_mock(None).await;
12873        let version = Version(0);
12874
12875        frontend.hack_set_program(&ctx, program).await.unwrap();
12876        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12877        let sketch_id = sketch_object.id;
12878        let sketch = expect_sketch(sketch_object);
12879        let point_ids = vec![
12880            sketch.segments.first().unwrap().to_owned(),
12881            sketch.segments.get(1).unwrap().to_owned(),
12882        ];
12883
12884        let constraint = Constraint::Vertical(Vertical::Points {
12885            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12886        });
12887        let (src_delta, scene_delta) = frontend
12888            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12889            .await
12890            .unwrap();
12891        assert_eq!(
12892            src_delta.text.as_str(),
12893            "\
12894sketch001 = sketch(on = XY) {
12895  p0 = point(at = [var 4mm, var 3.1mm])
12896  pf = point(at = [4, 4])
12897  vertical([p0, pf])
12898}
12899"
12900        );
12901        assert_eq!(
12902            scene_delta.new_graph.objects.len(),
12903            5,
12904            "{:#?}",
12905            scene_delta.new_graph.objects
12906        );
12907
12908        ctx.close().await;
12909        mock_ctx.close().await;
12910    }
12911
12912    #[tokio::test(flavor = "multi_thread")]
12913    async fn test_points_horizontal() {
12914        let initial_source = "\
12915sketch001 = sketch(on = XY) {
12916  p0 = point(at = [var -2.23mm, var 3.1mm])
12917  pf = point(at = [4, 4])
12918}
12919";
12920
12921        let program = Program::parse(initial_source).unwrap().0.unwrap();
12922
12923        let mut frontend = FrontendState::new();
12924
12925        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12926        let mock_ctx = ExecutorContext::new_mock(None).await;
12927        let version = Version(0);
12928
12929        frontend.hack_set_program(&ctx, program).await.unwrap();
12930        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12931        let sketch_id = sketch_object.id;
12932        let sketch = expect_sketch(sketch_object);
12933        let point_ids = vec![
12934            sketch.segments.first().unwrap().to_owned(),
12935            sketch.segments.get(1).unwrap().to_owned(),
12936        ];
12937
12938        let constraint = Constraint::Horizontal(Horizontal::Points {
12939            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12940        });
12941        let (src_delta, scene_delta) = frontend
12942            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12943            .await
12944            .unwrap();
12945        assert_eq!(
12946            src_delta.text.as_str(),
12947            "\
12948sketch001 = sketch(on = XY) {
12949  p0 = point(at = [var -2.23mm, var 4mm])
12950  pf = point(at = [4, 4])
12951  horizontal([p0, pf])
12952}
12953"
12954        );
12955        assert_eq!(
12956            scene_delta.new_graph.objects.len(),
12957            5,
12958            "{:#?}",
12959            scene_delta.new_graph.objects
12960        );
12961
12962        ctx.close().await;
12963        mock_ctx.close().await;
12964    }
12965
12966    #[tokio::test(flavor = "multi_thread")]
12967    async fn test_point_horizontal_with_origin() {
12968        let initial_source = "\
12969sketch001 = sketch(on = XY) {
12970  p0 = point(at = [var -2.23mm, var 3.1mm])
12971}
12972";
12973
12974        let program = Program::parse(initial_source).unwrap().0.unwrap();
12975
12976        let mut frontend = FrontendState::new();
12977
12978        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12979        let mock_ctx = ExecutorContext::new_mock(None).await;
12980        let version = Version(0);
12981
12982        frontend.hack_set_program(&ctx, program).await.unwrap();
12983        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12984        let sketch_id = sketch_object.id;
12985        let sketch = expect_sketch(sketch_object);
12986        let point_id = *sketch.segments.first().unwrap();
12987
12988        let constraint = Constraint::Horizontal(Horizontal::Points {
12989            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12990        });
12991        let (src_delta, scene_delta) = frontend
12992            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12993            .await
12994            .unwrap();
12995        assert_eq!(
12996            src_delta.text.as_str(),
12997            "\
12998sketch001 = sketch(on = XY) {
12999  p0 = point(at = [var -2.23mm, var 0mm])
13000  horizontal([p0, ORIGIN])
13001}
13002"
13003        );
13004        assert_eq!(
13005            scene_delta.new_graph.objects.len(),
13006            4,
13007            "{:#?}",
13008            scene_delta.new_graph.objects
13009        );
13010
13011        ctx.close().await;
13012        mock_ctx.close().await;
13013    }
13014
13015    #[tokio::test(flavor = "multi_thread")]
13016    async fn test_lines_equal_length() {
13017        let initial_source = "\
13018sketch(on = XY) {
13019  line(start = [var 1, var 2], end = [var 3, var 4])
13020  line(start = [var 5, var 6], end = [var 7, var 8])
13021}
13022";
13023
13024        let program = Program::parse(initial_source).unwrap().0.unwrap();
13025
13026        let mut frontend = FrontendState::new();
13027
13028        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13029        let mock_ctx = ExecutorContext::new_mock(None).await;
13030        let version = Version(0);
13031
13032        frontend.hack_set_program(&ctx, program).await.unwrap();
13033        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13034        let sketch_id = sketch_object.id;
13035        let sketch = expect_sketch(sketch_object);
13036        let line1_id = *sketch.segments.get(2).unwrap();
13037        let line2_id = *sketch.segments.get(5).unwrap();
13038
13039        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13040            lines: vec![line1_id, line2_id],
13041        });
13042        let (src_delta, scene_delta) = frontend
13043            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13044            .await
13045            .unwrap();
13046        assert_eq!(
13047            src_delta.text.as_str(),
13048            "\
13049sketch(on = XY) {
13050  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13051  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13052  equalLength([line1, line2])
13053}
13054"
13055        );
13056        assert_eq!(
13057            scene_delta.new_graph.objects.len(),
13058            9,
13059            "{:#?}",
13060            scene_delta.new_graph.objects
13061        );
13062
13063        ctx.close().await;
13064        mock_ctx.close().await;
13065    }
13066
13067    #[tokio::test(flavor = "multi_thread")]
13068    async fn test_add_constraint_multi_line_equal_length() {
13069        let initial_source = "\
13070sketch(on = XY) {
13071  line(start = [var 1, var 2], end = [var 3, var 4])
13072  line(start = [var 5, var 6], end = [var 7, var 8])
13073  line(start = [var 9, var 10], end = [var 11, var 12])
13074}
13075";
13076
13077        let program = Program::parse(initial_source).unwrap().0.unwrap();
13078
13079        let mut frontend = FrontendState::new();
13080        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13081        let mock_ctx = ExecutorContext::new_mock(None).await;
13082        let version = Version(0);
13083
13084        frontend.hack_set_program(&ctx, program).await.unwrap();
13085        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13086        let sketch_id = sketch_object.id;
13087        let sketch = expect_sketch(sketch_object);
13088        let line1_id = *sketch.segments.get(2).unwrap();
13089        let line2_id = *sketch.segments.get(5).unwrap();
13090        let line3_id = *sketch.segments.get(8).unwrap();
13091
13092        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13093            lines: vec![line1_id, line2_id, line3_id],
13094        });
13095        let (src_delta, scene_delta) = frontend
13096            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13097            .await
13098            .unwrap();
13099        assert_eq!(
13100            src_delta.text.as_str(),
13101            "\
13102sketch(on = XY) {
13103  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13104  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13105  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13106  equalLength([line1, line2, line3])
13107}
13108"
13109        );
13110        let constraints = scene_delta
13111            .new_graph
13112            .objects
13113            .iter()
13114            .filter_map(|obj| {
13115                let ObjectKind::Constraint { constraint } = &obj.kind else {
13116                    return None;
13117                };
13118                Some(constraint)
13119            })
13120            .collect::<Vec<_>>();
13121
13122        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13123        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13124            panic!("expected equal length constraint, got {:?}", constraints[0]);
13125        };
13126        assert_eq!(lines_equal_length.lines.len(), 3);
13127
13128        ctx.close().await;
13129        mock_ctx.close().await;
13130    }
13131
13132    #[tokio::test(flavor = "multi_thread")]
13133    async fn test_lines_parallel() {
13134        let initial_source = "\
13135sketch(on = XY) {
13136  line(start = [var 1, var 2], end = [var 3, var 4])
13137  line(start = [var 5, var 6], end = [var 7, var 8])
13138}
13139";
13140
13141        let program = Program::parse(initial_source).unwrap().0.unwrap();
13142
13143        let mut frontend = FrontendState::new();
13144
13145        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13146        let mock_ctx = ExecutorContext::new_mock(None).await;
13147        let version = Version(0);
13148
13149        frontend.hack_set_program(&ctx, program).await.unwrap();
13150        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13151        let sketch_id = sketch_object.id;
13152        let sketch = expect_sketch(sketch_object);
13153        let line1_id = *sketch.segments.get(2).unwrap();
13154        let line2_id = *sketch.segments.get(5).unwrap();
13155
13156        let constraint = Constraint::Parallel(Parallel {
13157            lines: vec![line1_id, line2_id],
13158        });
13159        let (src_delta, scene_delta) = frontend
13160            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13161            .await
13162            .unwrap();
13163        assert_eq!(
13164            src_delta.text.as_str(),
13165            "\
13166sketch(on = XY) {
13167  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13168  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13169  parallel([line1, line2])
13170}
13171"
13172        );
13173        assert_eq!(
13174            scene_delta.new_graph.objects.len(),
13175            9,
13176            "{:#?}",
13177            scene_delta.new_graph.objects
13178        );
13179
13180        ctx.close().await;
13181        mock_ctx.close().await;
13182    }
13183
13184    #[tokio::test(flavor = "multi_thread")]
13185    async fn test_lines_parallel_multiline() {
13186        let initial_source = "\
13187sketch(on = XY) {
13188  line(start = [var 1, var 2], end = [var 3, var 4])
13189  line(start = [var 5, var 6], end = [var 7, var 8])
13190  line(start = [var 9, var 10], end = [var 11, var 12])
13191}
13192";
13193
13194        let program = Program::parse(initial_source).unwrap().0.unwrap();
13195
13196        let mut frontend = FrontendState::new();
13197
13198        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13199        let mock_ctx = ExecutorContext::new_mock(None).await;
13200        let version = Version(0);
13201
13202        frontend.hack_set_program(&ctx, program).await.unwrap();
13203        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13204        let sketch_id = sketch_object.id;
13205        let sketch = expect_sketch(sketch_object);
13206        let line1_id = *sketch.segments.get(2).unwrap();
13207        let line2_id = *sketch.segments.get(5).unwrap();
13208        let line3_id = *sketch.segments.get(8).unwrap();
13209
13210        let constraint = Constraint::Parallel(Parallel {
13211            lines: vec![line1_id, line2_id, line3_id],
13212        });
13213        let (src_delta, scene_delta) = frontend
13214            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13215            .await
13216            .unwrap();
13217        assert_eq!(
13218            src_delta.text.as_str(),
13219            "\
13220sketch(on = XY) {
13221  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13222  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13223  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13224  parallel([line1, line2, line3])
13225}
13226"
13227        );
13228
13229        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13230        let sketch = expect_sketch(sketch_object);
13231        assert_eq!(sketch.constraints.len(), 1);
13232
13233        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13234        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13235            panic!("Expected constraint object");
13236        };
13237        let Constraint::Parallel(parallel) = constraint else {
13238            panic!("Expected parallel constraint");
13239        };
13240        assert_eq!(parallel.lines.len(), 3);
13241
13242        ctx.close().await;
13243        mock_ctx.close().await;
13244    }
13245
13246    #[tokio::test(flavor = "multi_thread")]
13247    async fn test_lines_perpendicular() {
13248        let initial_source = "\
13249sketch(on = XY) {
13250  line(start = [var 1, var 2], end = [var 3, var 4])
13251  line(start = [var 5, var 6], end = [var 7, var 8])
13252}
13253";
13254
13255        let program = Program::parse(initial_source).unwrap().0.unwrap();
13256
13257        let mut frontend = FrontendState::new();
13258
13259        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13260        let mock_ctx = ExecutorContext::new_mock(None).await;
13261        let version = Version(0);
13262
13263        frontend.hack_set_program(&ctx, program).await.unwrap();
13264        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13265        let sketch_id = sketch_object.id;
13266        let sketch = expect_sketch(sketch_object);
13267        let line1_id = *sketch.segments.get(2).unwrap();
13268        let line2_id = *sketch.segments.get(5).unwrap();
13269
13270        let constraint = Constraint::Perpendicular(Perpendicular {
13271            lines: vec![line1_id, line2_id],
13272        });
13273        let (src_delta, scene_delta) = frontend
13274            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13275            .await
13276            .unwrap();
13277        assert_eq!(
13278            src_delta.text.as_str(),
13279            "\
13280sketch(on = XY) {
13281  line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13282  line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13283  perpendicular([line1, line2])
13284}
13285"
13286        );
13287        assert_eq!(
13288            scene_delta.new_graph.objects.len(),
13289            9,
13290            "{:#?}",
13291            scene_delta.new_graph.objects
13292        );
13293
13294        ctx.close().await;
13295        mock_ctx.close().await;
13296    }
13297
13298    #[tokio::test(flavor = "multi_thread")]
13299    async fn test_lines_angle() {
13300        let initial_source = "\
13301sketch(on = XY) {
13302  line(start = [var 1, var 2], end = [var 3, var 4])
13303  line(start = [var 5, var 6], end = [var 7, var 8])
13304}
13305";
13306
13307        let program = Program::parse(initial_source).unwrap().0.unwrap();
13308
13309        let mut frontend = FrontendState::new();
13310
13311        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13312        let mock_ctx = ExecutorContext::new_mock(None).await;
13313        let version = Version(0);
13314
13315        frontend.hack_set_program(&ctx, program).await.unwrap();
13316        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13317        let sketch_id = sketch_object.id;
13318        let sketch = expect_sketch(sketch_object);
13319        let line1_id = *sketch.segments.get(2).unwrap();
13320        let line2_id = *sketch.segments.get(5).unwrap();
13321
13322        let constraint = Constraint::Angle(Angle {
13323            lines: vec![line1_id, line2_id],
13324            angle: Number {
13325                value: 30.0,
13326                units: NumericSuffix::Deg,
13327            },
13328            source: Default::default(),
13329        });
13330        let (src_delta, scene_delta) = frontend
13331            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13332            .await
13333            .unwrap();
13334        assert_eq!(
13335            src_delta.text.as_str(),
13336            // The lack indentation is a formatter bug.
13337            "\
13338sketch(on = XY) {
13339  line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13340  line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13341  angle([line1, line2]) == 30deg
13342}
13343"
13344        );
13345        assert_eq!(
13346            scene_delta.new_graph.objects.len(),
13347            9,
13348            "{:#?}",
13349            scene_delta.new_graph.objects
13350        );
13351
13352        ctx.close().await;
13353        mock_ctx.close().await;
13354    }
13355
13356    #[tokio::test(flavor = "multi_thread")]
13357    async fn test_segments_tangent() {
13358        let initial_source = "\
13359sketch(on = XY) {
13360  line(start = [var 1, var 2], end = [var 3, var 4])
13361  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13362}
13363";
13364
13365        let program = Program::parse(initial_source).unwrap().0.unwrap();
13366
13367        let mut frontend = FrontendState::new();
13368
13369        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13370        let mock_ctx = ExecutorContext::new_mock(None).await;
13371        let version = Version(0);
13372
13373        frontend.hack_set_program(&ctx, program).await.unwrap();
13374        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13375        let sketch_id = sketch_object.id;
13376        let sketch = expect_sketch(sketch_object);
13377        let line1_id = *sketch.segments.get(2).unwrap();
13378        let arc1_id = *sketch.segments.get(6).unwrap();
13379
13380        let constraint = Constraint::Tangent(Tangent {
13381            input: vec![line1_id, arc1_id],
13382        });
13383        let (src_delta, scene_delta) = frontend
13384            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13385            .await
13386            .unwrap();
13387        assert_eq!(
13388            src_delta.text.as_str(),
13389            "\
13390sketch(on = XY) {
13391  line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13392  arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13393  tangent([line1, arc1])
13394}
13395"
13396        );
13397        assert_eq!(
13398            scene_delta.new_graph.objects.len(),
13399            10,
13400            "{:#?}",
13401            scene_delta.new_graph.objects
13402        );
13403
13404        ctx.close().await;
13405        mock_ctx.close().await;
13406    }
13407
13408    #[tokio::test(flavor = "multi_thread")]
13409    async fn test_point_midpoint() {
13410        let initial_source = "\
13411sketch(on = XY) {
13412  point(at = [var 1, var 1])
13413  line(start = [var 0, var 0], end = [var 6, var 4])
13414}
13415";
13416
13417        let program = Program::parse(initial_source).unwrap().0.unwrap();
13418
13419        let mut frontend = FrontendState::new();
13420
13421        let ctx = ExecutorContext::new_mock(None).await;
13422        let version = Version(0);
13423
13424        frontend.program = program.clone();
13425        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13426        frontend.update_state_after_exec(outcome, true);
13427        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13428        let sketch_id = sketch_object.id;
13429        let sketch = expect_sketch(sketch_object);
13430        let point_id = *sketch.segments.first().unwrap();
13431        let line_id = *sketch.segments.get(3).unwrap();
13432
13433        let constraint = Constraint::Midpoint(Midpoint {
13434            point: ConstraintSegment::from(point_id),
13435            segment: line_id,
13436        });
13437        let (src_delta, scene_delta) = frontend
13438            .add_constraint(&ctx, version, sketch_id, constraint)
13439            .await
13440            .unwrap();
13441        assert_eq!(
13442            src_delta.text.as_str(),
13443            "\
13444sketch(on = XY) {
13445  point1 = point(at = [var 2.33, var 1.67])
13446  line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13447  midpoint(line1, point = point1)
13448}
13449"
13450        );
13451        assert_eq!(
13452            scene_delta.new_graph.objects.len(),
13453            7,
13454            "{:#?}",
13455            scene_delta.new_graph.objects
13456        );
13457
13458        ctx.close().await;
13459    }
13460
13461    #[tokio::test(flavor = "multi_thread")]
13462    async fn test_segments_symmetric() {
13463        let initial_source = "\
13464sketch(on = XY) {
13465  line(start = [var 0, var 0], end = [var 0, var 4])
13466  line(start = [var 4, var 0], end = [var 4, var 4])
13467  line(start = [var 2, var -1], end = [var 2, var 5])
13468}
13469";
13470
13471        let program = Program::parse(initial_source).unwrap().0.unwrap();
13472
13473        let mut frontend = FrontendState::new();
13474
13475        let ctx = ExecutorContext::new_mock(None).await;
13476        let version = Version(0);
13477
13478        frontend.program = program.clone();
13479        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13480        frontend.update_state_after_exec(outcome, true);
13481        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13482        let sketch_id = sketch_object.id;
13483        let sketch = expect_sketch(sketch_object);
13484        let line1_id = *sketch.segments.get(2).unwrap();
13485        let line2_id = *sketch.segments.get(5).unwrap();
13486        let axis_id = *sketch.segments.get(8).unwrap();
13487
13488        let constraint = Constraint::Symmetric(Symmetric {
13489            input: vec![line1_id, line2_id],
13490            axis: axis_id,
13491        });
13492        let (src_delta, scene_delta) = frontend
13493            .add_constraint(&ctx, version, sketch_id, constraint)
13494            .await
13495            .unwrap();
13496        assert_eq!(
13497            src_delta.text.as_str(),
13498            "\
13499sketch(on = XY) {
13500  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13501  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13502  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13503  symmetric([line1, line2], axis = line3)
13504}
13505"
13506        );
13507        assert_eq!(
13508            scene_delta.new_graph.objects.len(),
13509            12,
13510            "{:#?}",
13511            scene_delta.new_graph.objects
13512        );
13513
13514        ctx.close().await;
13515    }
13516
13517    #[tokio::test(flavor = "multi_thread")]
13518    async fn test_point_arc_midpoint() {
13519        let initial_source = "\
13520sketch(on = XY) {
13521  point(at = [var 6, var 3])
13522  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13523}
13524";
13525
13526        let program = Program::parse(initial_source).unwrap().0.unwrap();
13527
13528        let mut frontend = FrontendState::new();
13529
13530        let ctx = ExecutorContext::new_mock(None).await;
13531        let version = Version(0);
13532
13533        frontend.program = program.clone();
13534        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13535        frontend.update_state_after_exec(outcome, true);
13536        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13537        let sketch_id = sketch_object.id;
13538        let sketch = expect_sketch(sketch_object);
13539        let point_id = *sketch.segments.first().unwrap();
13540        let arc_id = *sketch.segments.get(4).unwrap();
13541
13542        let constraint = Constraint::Midpoint(Midpoint {
13543            point: ConstraintSegment::from(point_id),
13544            segment: arc_id,
13545        });
13546        let (src_delta, scene_delta) = frontend
13547            .add_constraint(&ctx, version, sketch_id, constraint)
13548            .await
13549            .unwrap();
13550        assert_eq!(
13551            src_delta.text.as_str(),
13552            "\
13553sketch(on = XY) {
13554  point1 = point(at = [var 6, var 2.35])
13555  arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13556  midpoint(arc1, point = point1)
13557}
13558"
13559        );
13560        assert_eq!(
13561            scene_delta.new_graph.objects.len(),
13562            8,
13563            "{:#?}",
13564            scene_delta.new_graph.objects
13565        );
13566
13567        ctx.close().await;
13568    }
13569
13570    #[tokio::test(flavor = "multi_thread")]
13571    async fn test_origin_line_midpoint() {
13572        let initial_source = "\
13573sketch(on = XY) {
13574  line(start = [var 0, var 0], end = [var 6, var 4])
13575}
13576";
13577
13578        let program = Program::parse(initial_source).unwrap().0.unwrap();
13579
13580        let mut frontend = FrontendState::new();
13581
13582        let ctx = ExecutorContext::new_mock(None).await;
13583        let version = Version(0);
13584
13585        frontend.program = program.clone();
13586        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13587        frontend.update_state_after_exec(outcome, true);
13588        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13589        let sketch_id = sketch_object.id;
13590        let sketch = expect_sketch(sketch_object);
13591        let line_id = *sketch.segments.get(2).unwrap();
13592
13593        let constraint = Constraint::Midpoint(Midpoint {
13594            point: ConstraintSegment::ORIGIN,
13595            segment: line_id,
13596        });
13597        let (src_delta, scene_delta) = frontend
13598            .add_constraint(&ctx, version, sketch_id, constraint)
13599            .await
13600            .unwrap();
13601        assert_eq!(
13602            src_delta.text.as_str(),
13603            "\
13604sketch(on = XY) {
13605  line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13606  midpoint(line1, point = ORIGIN)
13607}
13608"
13609        );
13610        assert_eq!(
13611            scene_delta.new_graph.objects.len(),
13612            6,
13613            "{:#?}",
13614            scene_delta.new_graph.objects
13615        );
13616
13617        ctx.close().await;
13618    }
13619
13620    #[tokio::test(flavor = "multi_thread")]
13621    async fn test_origin_arc_midpoint() {
13622        let initial_source = "\
13623sketch(on = XY) {
13624  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13625}
13626";
13627
13628        let program = Program::parse(initial_source).unwrap().0.unwrap();
13629
13630        let mut frontend = FrontendState::new();
13631
13632        let ctx = ExecutorContext::new_mock(None).await;
13633        let version = Version(0);
13634
13635        frontend.program = program.clone();
13636        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13637        frontend.update_state_after_exec(outcome, true);
13638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13639        let sketch_id = sketch_object.id;
13640        let sketch = expect_sketch(sketch_object);
13641        let arc_id = *sketch.segments.get(3).unwrap();
13642
13643        let constraint = Constraint::Midpoint(Midpoint {
13644            point: ConstraintSegment::ORIGIN,
13645            segment: arc_id,
13646        });
13647        let (src_delta, scene_delta) = frontend
13648            .add_constraint(&ctx, version, sketch_id, constraint)
13649            .await
13650            .unwrap();
13651        assert_eq!(
13652            src_delta.text.as_str(),
13653            "\
13654sketch(on = XY) {
13655  arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13656  midpoint(arc1, point = ORIGIN)
13657}
13658"
13659        );
13660        assert_eq!(
13661            scene_delta.new_graph.objects.len(),
13662            7,
13663            "{:#?}",
13664            scene_delta.new_graph.objects
13665        );
13666
13667        ctx.close().await;
13668    }
13669
13670    #[tokio::test(flavor = "multi_thread")]
13671    async fn test_segments_symmetric_arcs() {
13672        let initial_source = "\
13673sketch(on = XY) {
13674  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13675  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13676  line(start = [var 0, var -10], end = [var 0, var 10])
13677}
13678";
13679
13680        let program = Program::parse(initial_source).unwrap().0.unwrap();
13681
13682        let mut frontend = FrontendState::new();
13683
13684        let ctx = ExecutorContext::new_mock(None).await;
13685        let version = Version(0);
13686
13687        frontend.program = program.clone();
13688        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13689        frontend.update_state_after_exec(outcome, true);
13690        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13691        let sketch_id = sketch_object.id;
13692        let sketch = expect_sketch(sketch_object);
13693        let arc1_id = *sketch.segments.get(3).unwrap();
13694        let arc2_id = *sketch.segments.get(7).unwrap();
13695        let axis_id = *sketch.segments.get(10).unwrap();
13696
13697        let constraint = Constraint::Symmetric(Symmetric {
13698            input: vec![arc1_id, arc2_id],
13699            axis: axis_id,
13700        });
13701        let (src_delta, scene_delta) = frontend
13702            .add_constraint(&ctx, version, sketch_id, constraint)
13703            .await
13704            .unwrap();
13705        assert_eq!(
13706            src_delta.text.as_str(),
13707            "\
13708sketch(on = XY) {
13709  arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13710  arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13711  line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13712  symmetric([arc1, arc2], axis = line1)
13713}
13714"
13715        );
13716        assert_eq!(
13717            scene_delta.new_graph.objects.len(),
13718            14,
13719            "{:#?}",
13720            scene_delta.new_graph.objects
13721        );
13722
13723        ctx.close().await;
13724    }
13725
13726    #[tokio::test(flavor = "multi_thread")]
13727    async fn test_sketch_on_face_simple() {
13728        let initial_source = "\
13729len = 2mm
13730cube = startSketchOn(XY)
13731  |> startProfile(at = [0, 0])
13732  |> line(end = [len, 0], tag = $side)
13733  |> line(end = [0, len])
13734  |> line(end = [-len, 0])
13735  |> line(end = [0, -len])
13736  |> close()
13737  |> extrude(length = len)
13738
13739face = faceOf(cube, face = side)
13740";
13741
13742        let program = Program::parse(initial_source).unwrap().0.unwrap();
13743
13744        let mut frontend = FrontendState::new();
13745
13746        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13747        let mock_ctx = ExecutorContext::new_mock(None).await;
13748        let version = Version(0);
13749
13750        frontend.hack_set_program(&ctx, program).await.unwrap();
13751        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13752        let face_id = face_object.id;
13753
13754        let sketch_args = SketchCtor {
13755            on: Plane::Object(face_id),
13756        };
13757        let (_src_delta, scene_delta, sketch_id) = frontend
13758            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13759            .await
13760            .unwrap();
13761        assert_eq!(sketch_id, ObjectId(2));
13762        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13763        let sketch_object = &scene_delta.new_graph.objects[2];
13764        assert_eq!(sketch_object.id, ObjectId(2));
13765        assert_eq!(
13766            sketch_object.kind,
13767            ObjectKind::Sketch(Sketch {
13768                args: SketchCtor {
13769                    on: Plane::Object(face_id),
13770                },
13771                plane: face_id,
13772                segments: vec![],
13773                constraints: vec![],
13774            })
13775        );
13776        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13777
13778        ctx.close().await;
13779        mock_ctx.close().await;
13780    }
13781
13782    #[tokio::test(flavor = "multi_thread")]
13783    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13784        let initial_source = "\
13785s = sketch(on = YZ) {
13786  line1 = line(start = [0, 0], end = [0, 1])
13787  line2 = line(start = [0, 1], end = [1, 1])
13788  line3 = line(start = [1, 1], end = [0, 0])
13789}
13790region001 = region(point = [0.1, 0.1], sketch = s)
13791extrude001 = extrude(region001, length = 5)
13792";
13793
13794        let program = Program::parse(initial_source).unwrap().0.unwrap();
13795
13796        let mut frontend = FrontendState::new();
13797        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13798        let version = Version(0);
13799
13800        frontend.hack_set_program(&ctx, program).await.unwrap();
13801        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13802
13803        let sketch_args = SketchCtor {
13804            on: Plane::Object(wall_object_id),
13805        };
13806        let (src_delta, _scene_delta, _sketch_id) = frontend
13807            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13808            .await
13809            .unwrap();
13810        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13811
13812        ctx.close().await;
13813    }
13814
13815    #[tokio::test(flavor = "multi_thread")]
13816    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13817        let initial_source = "\
13818sketch001 = sketch(on = YZ) {
13819  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13820  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13821  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13822  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13823  coincident([line1.end, line2.start])
13824  coincident([line2.end, line3.start])
13825  coincident([line3.end, line4.start])
13826  coincident([line4.end, line1.start])
13827  parallel([line2, line4])
13828  parallel([line3, line1])
13829  perpendicular([line1, line2])
13830  horizontal(line3)
13831  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13832}
13833region001 = region(point = [3.1, 3.74], sketch = sketch001)
13834extrude001 = extrude(region001, length = 5)
13835";
13836
13837        let program = Program::parse(initial_source).unwrap().0.unwrap();
13838
13839        let mut frontend = FrontendState::new();
13840        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13841        let version = Version(0);
13842
13843        frontend.hack_set_program(&ctx, program).await.unwrap();
13844        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13845
13846        let sketch_args = SketchCtor {
13847            on: Plane::Object(wall_object_id),
13848        };
13849        let (src_delta, _scene_delta, _sketch_id) = frontend
13850            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13851            .await
13852            .unwrap();
13853        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13854
13855        ctx.close().await;
13856    }
13857
13858    #[test]
13859    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13860        let source = "\
13861sketch001 = sketch(on = XY) {
13862  line(start = [0, 0], end = [1, 0])
13863}
13864part = subtract(boxSolid, tools = [cutSolid])
13865  |> appearance(color = \"#8f96a3\")
13866";
13867        let ast = Program::parse(source).unwrap().0.unwrap().ast;
13868        let line_start = source.find("line").unwrap();
13869        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13870        let line_ref = SourceRef::Simple {
13871            range: [line_start, line_end, 0].into(),
13872            node_path: None,
13873        };
13874        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13875
13876        let subtract_start = source.find("subtract").unwrap();
13877        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13878        let subtract_ref = SourceRef::Simple {
13879            range: [subtract_start, subtract_end, 0].into(),
13880            node_path: None,
13881        };
13882        assert_eq!(
13883            variable_name_containing_source_ref(&ast, &subtract_ref),
13884            Some("part".to_owned())
13885        );
13886    }
13887
13888    #[tokio::test(flavor = "multi_thread")]
13889    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13890        clear_mem_cache().await;
13891        let source = "\
13892boxSolid = startSketchOn(XY)
13893  |> startProfile(at = [0, 0])
13894  |> line(end = [4, 0], tag = $bottomEdge)
13895  |> line(end = [0, 4])
13896  |> line(end = [-4, 0])
13897  |> close()
13898  |> extrude(length = 10)
13899cutSolid = startSketchOn(XY)
13900  |> startProfile(at = [1, 1])
13901  |> line(end = [1, 0])
13902  |> line(end = [0, 1])
13903  |> line(end = [-1, 0])
13904  |> close()
13905  |> extrude(length = 10)
13906part = subtract(boxSolid, tools = [cutSolid])
13907  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13908";
13909        let program = Program::parse(source).unwrap().0.unwrap();
13910        let mut frontend = FrontendState::new();
13911        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13912        match frontend.hack_set_program(&ctx, program).await.unwrap() {
13913            SetProgramOutcome::Success { .. } => {}
13914            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13915        }
13916
13917        let sweep_call_start = source.find("extrude").unwrap();
13918        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13919        let part_call_start = source.find("subtract").unwrap();
13920        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13921        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13922        let composite_range = [part_call_start, part_call_end, 0].into();
13923
13924        let cap_object = frontend
13925            .scene_graph
13926            .objects
13927            .iter()
13928            .find(|object| {
13929                matches!(
13930                    object.kind,
13931                    ObjectKind::Cap(crate::frontend::api::Cap {
13932                        kind: crate::frontend::api::CapKind::End,
13933                        ..
13934                    })
13935                ) && matches!(
13936                    &object.source,
13937                    SourceRef::BackTrace { ranges }
13938                        if ranges.len() == 2 && ranges[0].0 == composite_range && ranges[1].0 == sweep_range
13939                )
13940            })
13941            .expect("expected end cap object to trace through subtract and original extrude");
13942
13943        let mut ast = frontend.program.ast.clone();
13944        let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13945        let cap_face_decl = ast::VariableDeclaration::new(
13946            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13947            ast::ItemVisibility::Default,
13948            ast::VariableKind::Const,
13949        );
13950        ast.body
13951            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13952                cap_face_decl,
13953            ))));
13954        let generated_source = source_from_ast(&ast);
13955
13956        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13957        assert!(!generated_source.contains("faceOf(boxSolid"));
13958        let ast::Expr::CallExpressionKw(call) = cap_expr else {
13959            panic!("expected faceOf call");
13960        };
13961        assert_eq!(call.callee.name.name, "faceOf");
13962        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13963            panic!("expected solid name");
13964        };
13965        assert_eq!(solid_name.name.name, "part");
13966        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13967            panic!("expected face name");
13968        };
13969        assert_eq!(face_name.name.name, "END");
13970
13971        ctx.close().await;
13972    }
13973
13974    #[tokio::test(flavor = "multi_thread")]
13975    async fn test_sketch_on_plane_incremental() {
13976        let initial_source = "\
13977len = 2mm
13978cube = startSketchOn(XY)
13979  |> startProfile(at = [0, 0])
13980  |> line(end = [len, 0], tag = $side)
13981  |> line(end = [0, len])
13982  |> line(end = [-len, 0])
13983  |> line(end = [0, -len])
13984  |> close()
13985  |> extrude(length = len)
13986
13987plane = planeOf(cube, face = side)
13988";
13989
13990        let program = Program::parse(initial_source).unwrap().0.unwrap();
13991
13992        let mut frontend = FrontendState::new();
13993
13994        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13995        let mock_ctx = ExecutorContext::new_mock(None).await;
13996        let version = Version(0);
13997
13998        frontend.hack_set_program(&ctx, program).await.unwrap();
13999        // Find the last plane since the first plane is the XY plane.
14000        let plane_object = frontend
14001            .scene_graph
14002            .objects
14003            .iter()
14004            .rev()
14005            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14006            .unwrap();
14007        let plane_id = plane_object.id;
14008
14009        let sketch_args = SketchCtor {
14010            on: Plane::Object(plane_id),
14011        };
14012        let (src_delta, scene_delta, sketch_id) = frontend
14013            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14014            .await
14015            .unwrap();
14016        assert_eq!(
14017            src_delta.text.as_str(),
14018            "\
14019len = 2mm
14020cube = startSketchOn(XY)
14021  |> startProfile(at = [0, 0])
14022  |> line(end = [len, 0], tag = $side)
14023  |> line(end = [0, len])
14024  |> line(end = [-len, 0])
14025  |> line(end = [0, -len])
14026  |> close()
14027  |> extrude(length = len)
14028
14029plane = planeOf(cube, face = side)
14030sketch001 = sketch(on = plane) {
14031}
14032"
14033        );
14034        assert_eq!(sketch_id, ObjectId(2));
14035        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14036        let sketch_object = &scene_delta.new_graph.objects[2];
14037        assert_eq!(sketch_object.id, ObjectId(2));
14038        assert_eq!(
14039            sketch_object.kind,
14040            ObjectKind::Sketch(Sketch {
14041                args: SketchCtor {
14042                    on: Plane::Object(plane_id),
14043                },
14044                plane: plane_id,
14045                segments: vec![],
14046                constraints: vec![],
14047            })
14048        );
14049        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14050
14051        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14052        assert_eq!(plane_object.id, plane_id);
14053        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14054
14055        ctx.close().await;
14056        mock_ctx.close().await;
14057    }
14058
14059    #[tokio::test(flavor = "multi_thread")]
14060    async fn test_new_sketch_uses_unique_variable_name() {
14061        let initial_source = "\
14062sketch1 = sketch(on = XY) {
14063}
14064";
14065
14066        let program = Program::parse(initial_source).unwrap().0.unwrap();
14067
14068        let mut frontend = FrontendState::new();
14069        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14070        let version = Version(0);
14071
14072        frontend.hack_set_program(&ctx, program).await.unwrap();
14073
14074        let sketch_args = SketchCtor {
14075            on: Plane::Default(PlaneName::Yz),
14076        };
14077        let (src_delta, _, _) = frontend
14078            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14079            .await
14080            .unwrap();
14081
14082        assert_eq!(
14083            src_delta.text.as_str(),
14084            "\
14085sketch1 = sketch(on = XY) {
14086}
14087sketch001 = sketch(on = YZ) {
14088}
14089"
14090        );
14091
14092        ctx.close().await;
14093    }
14094
14095    #[tokio::test(flavor = "multi_thread")]
14096    async fn test_new_sketch_twice_using_same_plane() {
14097        let initial_source = "\
14098sketch1 = sketch(on = XY) {
14099}
14100";
14101
14102        let program = Program::parse(initial_source).unwrap().0.unwrap();
14103
14104        let mut frontend = FrontendState::new();
14105        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14106        let version = Version(0);
14107
14108        frontend.hack_set_program(&ctx, program).await.unwrap();
14109
14110        let sketch_args = SketchCtor {
14111            on: Plane::Default(PlaneName::Xy),
14112        };
14113        let (src_delta, _, _) = frontend
14114            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14115            .await
14116            .unwrap();
14117
14118        assert_eq!(
14119            src_delta.text.as_str(),
14120            "\
14121sketch1 = sketch(on = XY) {
14122}
14123sketch001 = sketch(on = XY) {
14124}
14125"
14126        );
14127
14128        ctx.close().await;
14129    }
14130
14131    #[tokio::test(flavor = "multi_thread")]
14132    async fn test_sketch_mode_reuses_cached_on_expression() {
14133        let initial_source = "\
14134width = 2mm
14135sketch(on = offsetPlane(XY, offset = width)) {
14136  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14137  distance([line1.start, line1.end]) == width
14138}
14139";
14140        let program = Program::parse(initial_source).unwrap().0.unwrap();
14141
14142        let mut frontend = FrontendState::new();
14143        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14144        let mock_ctx = ExecutorContext::new_mock(None).await;
14145        let version = Version(0);
14146        let project_id = ProjectId(0);
14147        let file_id = FileId(0);
14148
14149        frontend.hack_set_program(&ctx, program).await.unwrap();
14150        let initial_object_count = frontend.scene_graph.objects.len();
14151        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14152            .expect("Expected sketch object to exist")
14153            .id;
14154
14155        // Entering sketch mode should reuse cached `on` expression state
14156        // (offsetPlane result), not fail or create extra on-surface objects.
14157        let scene_delta = frontend
14158            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14159            .await
14160            .unwrap();
14161        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14162
14163        // A follow-up sketch-mode execution should keep the same stable object
14164        // graph shape as well.
14165        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14166        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14167
14168        ctx.close().await;
14169        mock_ctx.close().await;
14170    }
14171
14172    #[tokio::test(flavor = "multi_thread")]
14173    async fn test_multiple_sketch_blocks() {
14174        let initial_source = "\
14175// Cube that requires the engine.
14176width = 2
14177sketch001 = startSketchOn(XY)
14178profile001 = startProfile(sketch001, at = [0, 0])
14179  |> yLine(length = width, tag = $seg1)
14180  |> xLine(length = width)
14181  |> yLine(length = -width)
14182  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14183  |> close()
14184extrude001 = extrude(profile001, length = width)
14185
14186// Get a value that requires the engine.
14187x = segLen(seg1)
14188
14189// Triangle with side length 2*x.
14190sketch(on = XY) {
14191  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14192  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14193  coincident([line1.end, line2.start])
14194  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14195  coincident([line2.end, line3.start])
14196  coincident([line3.end, line1.start])
14197  equalLength([line3, line1])
14198  equalLength([line1, line2])
14199  distance([line1.start, line1.end]) == 2*x
14200}
14201
14202// Line segment with length x.
14203sketch2 = sketch(on = XY) {
14204  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14205  distance([line1.start, line1.end]) == x
14206}
14207";
14208
14209        let program = Program::parse(initial_source).unwrap().0.unwrap();
14210
14211        let mut frontend = FrontendState::new();
14212
14213        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14214        let mock_ctx = ExecutorContext::new_mock(None).await;
14215        let version = Version(0);
14216        let project_id = ProjectId(0);
14217        let file_id = FileId(0);
14218
14219        frontend.hack_set_program(&ctx, program).await.unwrap();
14220        let sketch_objects = frontend
14221            .scene_graph
14222            .objects
14223            .iter()
14224            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14225            .collect::<Vec<_>>();
14226        let sketch1_id = sketch_objects.first().unwrap().id;
14227        let sketch2_id = sketch_objects.get(1).unwrap().id;
14228        // First point in sketch1.
14229        let point1_id = ObjectId(sketch1_id.0 + 1);
14230        // First point in sketch2.
14231        let point2_id = ObjectId(sketch2_id.0 + 1);
14232
14233        // Edit the first sketch. Objects before the sketch block should be
14234        // present from execution cache so that we can sketch on prior planes,
14235        // for example. Objects after the first sketch block should not be
14236        // present since those statements are skipped in sketch mode.
14237        //
14238        // - startSketchOn(XY) Plane 1
14239        // - sketch on=XY Plane 1
14240        // - Sketch block 16
14241        let scene_delta = frontend
14242            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14243            .await
14244            .unwrap();
14245        assert_eq!(
14246            scene_delta.new_graph.objects.len(),
14247            18,
14248            "{:#?}",
14249            scene_delta.new_graph.objects
14250        );
14251
14252        // Edit a point in the first sketch.
14253        let point_ctor = PointCtor {
14254            position: Point2d {
14255                x: Expr::Var(Number {
14256                    value: 1.0,
14257                    units: NumericSuffix::Mm,
14258                }),
14259                y: Expr::Var(Number {
14260                    value: 2.0,
14261                    units: NumericSuffix::Mm,
14262                }),
14263            },
14264        };
14265        let segments = vec![ExistingSegmentCtor {
14266            id: point1_id,
14267            ctor: SegmentCtor::Point(point_ctor),
14268        }];
14269        let (src_delta, _) = frontend
14270            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14271            .await
14272            .unwrap();
14273        // Only the first sketch block changes.
14274        assert_eq!(
14275            src_delta.text.as_str(),
14276            "\
14277// Cube that requires the engine.
14278width = 2
14279sketch001 = startSketchOn(XY)
14280profile001 = startProfile(sketch001, at = [0, 0])
14281  |> yLine(length = width, tag = $seg1)
14282  |> xLine(length = width)
14283  |> yLine(length = -width)
14284  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14285  |> close()
14286extrude001 = extrude(profile001, length = width)
14287
14288// Get a value that requires the engine.
14289x = segLen(seg1)
14290
14291// Triangle with side length 2*x.
14292sketch(on = XY) {
14293  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14294  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14295  coincident([line1.end, line2.start])
14296  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14297  coincident([line2.end, line3.start])
14298  coincident([line3.end, line1.start])
14299  equalLength([line3, line1])
14300  equalLength([line1, line2])
14301  distance([line1.start, line1.end]) == 2 * x
14302}
14303
14304// Line segment with length x.
14305sketch2 = sketch(on = XY) {
14306  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14307  distance([line1.start, line1.end]) == x
14308}
14309"
14310        );
14311        let edited_sketch1_source = src_delta.text.clone();
14312
14313        // Execute mock to simulate drag end.
14314        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14315        assert_eq!(src_delta.text, edited_sketch1_source);
14316        // Exit sketch. Objects from the entire program should be present.
14317        //
14318        // - startSketchOn(XY) Plane 1
14319        // - sketch on=XY Plane 1
14320        // - Sketch block 16
14321        // - sketch on=XY cached
14322        // - Sketch block 5
14323        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14324        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14325
14326        // Edit the second sketch.
14327        //
14328        // - startSketchOn(XY) Plane 1
14329        // - sketch on=XY Plane 1
14330        // - Sketch block 16
14331        // - sketch on=XY cached
14332        // - Sketch block 5
14333        let scene_delta = frontend
14334            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14335            .await
14336            .unwrap();
14337        assert_eq!(
14338            scene_delta.new_graph.objects.len(),
14339            24,
14340            "{:#?}",
14341            scene_delta.new_graph.objects
14342        );
14343
14344        // Edit a point in the second sketch.
14345        let point_ctor = PointCtor {
14346            position: Point2d {
14347                x: Expr::Var(Number {
14348                    value: 3.0,
14349                    units: NumericSuffix::Mm,
14350                }),
14351                y: Expr::Var(Number {
14352                    value: 4.0,
14353                    units: NumericSuffix::Mm,
14354                }),
14355            },
14356        };
14357        let segments = vec![ExistingSegmentCtor {
14358            id: point2_id,
14359            ctor: SegmentCtor::Point(point_ctor),
14360        }];
14361        let (src_delta, _) = frontend
14362            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14363            .await
14364            .unwrap();
14365        // Only the second sketch block changes.
14366        assert_eq!(
14367            src_delta.text.as_str(),
14368            "\
14369// Cube that requires the engine.
14370width = 2
14371sketch001 = startSketchOn(XY)
14372profile001 = startProfile(sketch001, at = [0, 0])
14373  |> yLine(length = width, tag = $seg1)
14374  |> xLine(length = width)
14375  |> yLine(length = -width)
14376  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14377  |> close()
14378extrude001 = extrude(profile001, length = width)
14379
14380// Get a value that requires the engine.
14381x = segLen(seg1)
14382
14383// Triangle with side length 2*x.
14384sketch(on = XY) {
14385  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14386  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14387  coincident([line1.end, line2.start])
14388  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14389  coincident([line2.end, line3.start])
14390  coincident([line3.end, line1.start])
14391  equalLength([line3, line1])
14392  equalLength([line1, line2])
14393  distance([line1.start, line1.end]) == 2 * x
14394}
14395
14396// Line segment with length x.
14397sketch2 = sketch(on = XY) {
14398  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14399  distance([line1.start, line1.end]) == x
14400}
14401"
14402        );
14403        let edited_sketch2_source = src_delta.text.clone();
14404
14405        // Execute mock to simulate drag end.
14406        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14407        assert_eq!(src_delta.text, edited_sketch2_source);
14408
14409        ctx.close().await;
14410        mock_ctx.close().await;
14411    }
14412
14413    #[tokio::test(flavor = "multi_thread")]
14414    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14415        clear_mem_cache().await;
14416
14417        let source = r#"sketch001 = sketch(on = XZ) {
14418  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14419}
14420sketch002 = sketch(on = XY) {
14421  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14422  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14423  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14424  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14425  coincident([line1.end, line2.start])
14426  coincident([line2.end, line3.start])
14427  coincident([line3.end, line4.start])
14428  coincident([line4.end, line1.start])
14429  parallel([line2, line4])
14430  parallel([line3, line1])
14431  perpendicular([line1, line2])
14432  horizontal(line3)
14433  coincident([line1.start, ORIGIN])
14434}
14435"#;
14436
14437        let program = Program::parse(source).unwrap().0.unwrap();
14438        let mut frontend = FrontendState::new();
14439        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14440        let mock_ctx = ExecutorContext::new_mock(None).await;
14441        let version = Version(0);
14442        let project_id = ProjectId(0);
14443        let file_id = FileId(0);
14444
14445        frontend.hack_set_program(&ctx, program).await.unwrap();
14446        let sketch_objects = frontend
14447            .scene_graph
14448            .objects
14449            .iter()
14450            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14451            .collect::<Vec<_>>();
14452        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14453
14454        let sketch1_id = sketch_objects[0].id;
14455        let sketch2_id = sketch_objects[1].id;
14456
14457        frontend
14458            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14459            .await
14460            .unwrap();
14461        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14462
14463        let scene_delta = frontend
14464            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14465            .await
14466            .unwrap();
14467        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14468
14469        clear_mem_cache().await;
14470        ctx.close().await;
14471        mock_ctx.close().await;
14472    }
14473
14474    // Regression tests: operations on source code with extra whitespace/newlines.
14475    // These test that NodePath-based lookups work correctly when source ranges
14476    // are shifted by extra whitespace that wouldn't be present after formatting.
14477
14478    #[tokio::test(flavor = "multi_thread")]
14479    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14480        // Extra newlines after @settings line - this shifts all source ranges.
14481        let initial_source = "@settings(defaultLengthUnit = mm)
14482
14483
14484
14485sketch001 = sketch(on = XY) {
14486  point(at = [1in, 2in])
14487}
14488";
14489
14490        let program = Program::parse(initial_source).unwrap().0.unwrap();
14491        let mut frontend = FrontendState::new();
14492
14493        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14494        let mock_ctx = ExecutorContext::new_mock(None).await;
14495        let version = Version(0);
14496        let project_id = ProjectId(0);
14497        let file_id = FileId(0);
14498
14499        frontend.hack_set_program(&ctx, program).await.unwrap();
14500        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14501        let sketch_id = sketch_object.id;
14502
14503        // Edit sketch should succeed despite extra newlines.
14504        frontend
14505            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14506            .await
14507            .unwrap();
14508
14509        // Add a new point to the sketch.
14510        let point_ctor = PointCtor {
14511            position: Point2d {
14512                x: Expr::Number(Number {
14513                    value: 5.0,
14514                    units: NumericSuffix::Mm,
14515                }),
14516                y: Expr::Number(Number {
14517                    value: 6.0,
14518                    units: NumericSuffix::Mm,
14519                }),
14520            },
14521        };
14522        let segment = SegmentCtor::Point(point_ctor);
14523        let (src_delta, scene_delta) = frontend
14524            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14525            .await
14526            .unwrap();
14527        // After adding a point, the source should be reformatted with standard whitespace.
14528        assert!(
14529            src_delta.text.contains("point(at = [5mm, 6mm])"),
14530            "Expected new point in source, got: {}",
14531            src_delta.text
14532        );
14533        assert!(!scene_delta.new_objects.is_empty());
14534
14535        ctx.close().await;
14536        mock_ctx.close().await;
14537    }
14538
14539    #[tokio::test(flavor = "multi_thread")]
14540    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14541        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14542
14543        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14544        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14545
14546        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14547        assert!(
14548            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14549            "Expected experimental settings to be added to source"
14550        );
14551    }
14552
14553    #[tokio::test(flavor = "multi_thread")]
14554    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14555        // Extra newlines after @settings, with an empty sketch block.
14556        let initial_source = "@settings(defaultLengthUnit = mm)
14557
14558
14559
14560s = sketch(on = XY) {}
14561";
14562
14563        let program = Program::parse(initial_source).unwrap().0.unwrap();
14564        let mut frontend = FrontendState::new();
14565
14566        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14567        let mock_ctx = ExecutorContext::new_mock(None).await;
14568        let version = Version(0);
14569
14570        frontend.hack_set_program(&ctx, program).await.unwrap();
14571        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14572        let sketch_id = sketch_object.id;
14573
14574        let line_ctor = LineCtor {
14575            start: Point2d {
14576                x: Expr::Number(Number {
14577                    value: 0.0,
14578                    units: NumericSuffix::Mm,
14579                }),
14580                y: Expr::Number(Number {
14581                    value: 0.0,
14582                    units: NumericSuffix::Mm,
14583                }),
14584            },
14585            end: Point2d {
14586                x: Expr::Number(Number {
14587                    value: 10.0,
14588                    units: NumericSuffix::Mm,
14589                }),
14590                y: Expr::Number(Number {
14591                    value: 10.0,
14592                    units: NumericSuffix::Mm,
14593                }),
14594            },
14595            construction: None,
14596        };
14597        let segment = SegmentCtor::Line(line_ctor);
14598        let (src_delta, scene_delta) = frontend
14599            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14600            .await
14601            .unwrap();
14602        assert!(
14603            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14604            "Expected line in source, got: {}",
14605            src_delta.text
14606        );
14607        // Line creates start point, end point, and line segment.
14608        assert_eq!(scene_delta.new_objects.len(), 3);
14609
14610        ctx.close().await;
14611        mock_ctx.close().await;
14612    }
14613
14614    #[tokio::test(flavor = "multi_thread")]
14615    async fn test_extra_newlines_between_operations_edit_line() {
14616        // Extra newlines between @settings and sketch, and inside the sketch block.
14617        let initial_source = "@settings(defaultLengthUnit = mm)
14618
14619
14620sketch001 = sketch(on = XY) {
14621
14622  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14623
14624}
14625";
14626
14627        let program = Program::parse(initial_source).unwrap().0.unwrap();
14628        let mut frontend = FrontendState::new();
14629
14630        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14631        let mock_ctx = ExecutorContext::new_mock(None).await;
14632        let version = Version(0);
14633        let project_id = ProjectId(0);
14634        let file_id = FileId(0);
14635
14636        frontend.hack_set_program(&ctx, program).await.unwrap();
14637        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14638        let sketch_id = sketch_object.id;
14639        let sketch = expect_sketch(sketch_object);
14640
14641        // Extract segment IDs before edit_sketch borrows frontend mutably.
14642        let line_id = sketch
14643            .segments
14644            .iter()
14645            .copied()
14646            .find(|seg_id| {
14647                matches!(
14648                    &frontend.scene_graph.objects[seg_id.0].kind,
14649                    ObjectKind::Segment {
14650                        segment: Segment::Line(_)
14651                    }
14652                )
14653            })
14654            .expect("Expected a line segment in sketch");
14655
14656        // Enter sketch edit mode.
14657        frontend
14658            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14659            .await
14660            .unwrap();
14661
14662        // Edit the line.
14663        let line_ctor = LineCtor {
14664            start: Point2d {
14665                x: Expr::Var(Number {
14666                    value: 1.0,
14667                    units: NumericSuffix::Mm,
14668                }),
14669                y: Expr::Var(Number {
14670                    value: 2.0,
14671                    units: NumericSuffix::Mm,
14672                }),
14673            },
14674            end: Point2d {
14675                x: Expr::Var(Number {
14676                    value: 13.0,
14677                    units: NumericSuffix::Mm,
14678                }),
14679                y: Expr::Var(Number {
14680                    value: 14.0,
14681                    units: NumericSuffix::Mm,
14682                }),
14683            },
14684            construction: None,
14685        };
14686        let segments = vec![ExistingSegmentCtor {
14687            id: line_id,
14688            ctor: SegmentCtor::Line(line_ctor),
14689        }];
14690        let (src_delta, _scene_delta) = frontend
14691            .edit_segments(&mock_ctx, version, sketch_id, segments)
14692            .await
14693            .unwrap();
14694        assert!(
14695            src_delta
14696                .text
14697                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14698            "Expected edited line in source, got: {}",
14699            src_delta.text
14700        );
14701
14702        ctx.close().await;
14703        mock_ctx.close().await;
14704    }
14705
14706    #[tokio::test(flavor = "multi_thread")]
14707    async fn test_extra_newlines_delete_segment() {
14708        // Extra whitespace before and after the sketch block.
14709        let initial_source = "@settings(defaultLengthUnit = mm)
14710
14711
14712
14713sketch001 = sketch(on = XY) {
14714  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14715}
14716";
14717
14718        let program = Program::parse(initial_source).unwrap().0.unwrap();
14719        let mut frontend = FrontendState::new();
14720
14721        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14722        let mock_ctx = ExecutorContext::new_mock(None).await;
14723        let version = Version(0);
14724
14725        frontend.hack_set_program(&ctx, program).await.unwrap();
14726        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14727        let sketch_id = sketch_object.id;
14728        let sketch = expect_sketch(sketch_object);
14729
14730        // The sketch should have 3 segments: start point, center point, and the circle.
14731        assert_eq!(sketch.segments.len(), 3);
14732        let circle_id = sketch.segments[2];
14733
14734        // Delete the circle despite extra newlines in original source.
14735        let (src_delta, scene_delta) = frontend
14736            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14737            .await
14738            .unwrap();
14739        assert!(
14740            src_delta.text.contains("sketch(on = XY) {"),
14741            "Expected sketch block in source, got: {}",
14742            src_delta.text
14743        );
14744        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14745        let new_sketch = expect_sketch(new_sketch_object);
14746        assert_eq!(new_sketch.segments.len(), 0);
14747
14748        ctx.close().await;
14749        mock_ctx.close().await;
14750    }
14751
14752    #[tokio::test(flavor = "multi_thread")]
14753    async fn test_unformatted_source_add_arc() {
14754        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14755        let initial_source = "@settings(defaultLengthUnit = mm)
14756
14757
14758
14759
14760sketch001 = sketch(on = XY) {
14761}
14762";
14763
14764        let program = Program::parse(initial_source).unwrap().0.unwrap();
14765        let mut frontend = FrontendState::new();
14766
14767        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14768        let mock_ctx = ExecutorContext::new_mock(None).await;
14769        let version = Version(0);
14770
14771        frontend.hack_set_program(&ctx, program).await.unwrap();
14772        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14773        let sketch_id = sketch_object.id;
14774
14775        let arc_ctor = ArcCtor {
14776            start: Point2d {
14777                x: Expr::Var(Number {
14778                    value: 5.0,
14779                    units: NumericSuffix::Mm,
14780                }),
14781                y: Expr::Var(Number {
14782                    value: 0.0,
14783                    units: NumericSuffix::Mm,
14784                }),
14785            },
14786            end: Point2d {
14787                x: Expr::Var(Number {
14788                    value: 0.0,
14789                    units: NumericSuffix::Mm,
14790                }),
14791                y: Expr::Var(Number {
14792                    value: 5.0,
14793                    units: NumericSuffix::Mm,
14794                }),
14795            },
14796            center: Point2d {
14797                x: Expr::Var(Number {
14798                    value: 0.0,
14799                    units: NumericSuffix::Mm,
14800                }),
14801                y: Expr::Var(Number {
14802                    value: 0.0,
14803                    units: NumericSuffix::Mm,
14804                }),
14805            },
14806            construction: None,
14807        };
14808        let segment = SegmentCtor::Arc(arc_ctor);
14809        let (src_delta, scene_delta) = frontend
14810            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14811            .await
14812            .unwrap();
14813        assert!(
14814            src_delta
14815                .text
14816                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14817            "Expected arc in source, got: {}",
14818            src_delta.text
14819        );
14820        assert!(!scene_delta.new_objects.is_empty());
14821
14822        ctx.close().await;
14823        mock_ctx.close().await;
14824    }
14825
14826    #[tokio::test(flavor = "multi_thread")]
14827    async fn test_extra_newlines_add_circle() {
14828        // Extra blank lines between settings and sketch.
14829        let initial_source = "@settings(defaultLengthUnit = mm)
14830
14831
14832
14833sketch001 = sketch(on = XY) {
14834}
14835";
14836
14837        let program = Program::parse(initial_source).unwrap().0.unwrap();
14838        let mut frontend = FrontendState::new();
14839
14840        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14841        let mock_ctx = ExecutorContext::new_mock(None).await;
14842        let version = Version(0);
14843
14844        frontend.hack_set_program(&ctx, program).await.unwrap();
14845        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14846        let sketch_id = sketch_object.id;
14847
14848        let circle_ctor = CircleCtor {
14849            start: Point2d {
14850                x: Expr::Var(Number {
14851                    value: 5.0,
14852                    units: NumericSuffix::Mm,
14853                }),
14854                y: Expr::Var(Number {
14855                    value: 0.0,
14856                    units: NumericSuffix::Mm,
14857                }),
14858            },
14859            center: Point2d {
14860                x: Expr::Var(Number {
14861                    value: 0.0,
14862                    units: NumericSuffix::Mm,
14863                }),
14864                y: Expr::Var(Number {
14865                    value: 0.0,
14866                    units: NumericSuffix::Mm,
14867                }),
14868            },
14869            construction: None,
14870        };
14871        let segment = SegmentCtor::Circle(circle_ctor);
14872        let (src_delta, scene_delta) = frontend
14873            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14874            .await
14875            .unwrap();
14876        assert!(
14877            src_delta
14878                .text
14879                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14880            "Expected circle in source, got: {}",
14881            src_delta.text
14882        );
14883        assert!(!scene_delta.new_objects.is_empty());
14884
14885        ctx.close().await;
14886        mock_ctx.close().await;
14887    }
14888
14889    #[tokio::test(flavor = "multi_thread")]
14890    async fn test_extra_newlines_add_constraint() {
14891        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14892        let initial_source = "@settings(defaultLengthUnit = mm)
14893
14894
14895
14896sketch001 = sketch(on = XY) {
14897  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14898  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14899}
14900";
14901
14902        let program = Program::parse(initial_source).unwrap().0.unwrap();
14903        let mut frontend = FrontendState::new();
14904
14905        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14906        let mock_ctx = ExecutorContext::new_mock(None).await;
14907        let version = Version(0);
14908        let project_id = ProjectId(0);
14909        let file_id = FileId(0);
14910
14911        frontend.hack_set_program(&ctx, program).await.unwrap();
14912        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14913        let sketch_id = sketch_object.id;
14914        let sketch = expect_sketch(sketch_object);
14915
14916        // Extract segment data before edit_sketch borrows frontend mutably.
14917        let line_ids: Vec<ObjectId> = sketch
14918            .segments
14919            .iter()
14920            .copied()
14921            .filter(|seg_id| {
14922                matches!(
14923                    &frontend.scene_graph.objects[seg_id.0].kind,
14924                    ObjectKind::Segment {
14925                        segment: Segment::Line(_)
14926                    }
14927                )
14928            })
14929            .collect();
14930        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14931
14932        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14933        let ObjectKind::Segment {
14934            segment: Segment::Line(line1_data),
14935        } = &line1.kind
14936        else {
14937            panic!("Expected line");
14938        };
14939        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14940        let ObjectKind::Segment {
14941            segment: Segment::Line(line2_data),
14942        } = &line2.kind
14943        else {
14944            panic!("Expected line");
14945        };
14946
14947        // Build constraint before entering sketch mode.
14948        let constraint = Constraint::Coincident(Coincident {
14949            segments: vec![line1_data.end.into(), line2_data.start.into()],
14950        });
14951
14952        // Enter sketch edit mode.
14953        frontend
14954            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14955            .await
14956            .unwrap();
14957        let (src_delta, _scene_delta) = frontend
14958            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14959            .await
14960            .unwrap();
14961        assert!(
14962            src_delta.text.contains("coincident("),
14963            "Expected coincident constraint in source, got: {}",
14964            src_delta.text
14965        );
14966
14967        ctx.close().await;
14968        mock_ctx.close().await;
14969    }
14970
14971    #[tokio::test(flavor = "multi_thread")]
14972    async fn test_extra_newlines_add_line_then_edit_line() {
14973        // Extra newlines after @settings - add a line, then edit it.
14974        let initial_source = "@settings(defaultLengthUnit = mm)
14975
14976
14977
14978sketch001 = sketch(on = XY) {
14979}
14980";
14981
14982        let program = Program::parse(initial_source).unwrap().0.unwrap();
14983        let mut frontend = FrontendState::new();
14984
14985        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14986        let mock_ctx = ExecutorContext::new_mock(None).await;
14987        let version = Version(0);
14988
14989        frontend.hack_set_program(&ctx, program).await.unwrap();
14990        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14991        let sketch_id = sketch_object.id;
14992
14993        // Add a line.
14994        let line_ctor = LineCtor {
14995            start: Point2d {
14996                x: Expr::Number(Number {
14997                    value: 0.0,
14998                    units: NumericSuffix::Mm,
14999                }),
15000                y: Expr::Number(Number {
15001                    value: 0.0,
15002                    units: NumericSuffix::Mm,
15003                }),
15004            },
15005            end: Point2d {
15006                x: Expr::Number(Number {
15007                    value: 10.0,
15008                    units: NumericSuffix::Mm,
15009                }),
15010                y: Expr::Number(Number {
15011                    value: 10.0,
15012                    units: NumericSuffix::Mm,
15013                }),
15014            },
15015            construction: None,
15016        };
15017        let segment = SegmentCtor::Line(line_ctor);
15018        let (src_delta, scene_delta) = frontend
15019            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15020            .await
15021            .unwrap();
15022        assert!(
15023            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15024            "Expected line in source after add, got: {}",
15025            src_delta.text
15026        );
15027        // Line creates start point, end point, and line segment.
15028        let line_id = *scene_delta.new_objects.last().unwrap();
15029
15030        // Edit the line.
15031        let line_ctor = LineCtor {
15032            start: Point2d {
15033                x: Expr::Number(Number {
15034                    value: 1.0,
15035                    units: NumericSuffix::Mm,
15036                }),
15037                y: Expr::Number(Number {
15038                    value: 2.0,
15039                    units: NumericSuffix::Mm,
15040                }),
15041            },
15042            end: Point2d {
15043                x: Expr::Number(Number {
15044                    value: 13.0,
15045                    units: NumericSuffix::Mm,
15046                }),
15047                y: Expr::Number(Number {
15048                    value: 14.0,
15049                    units: NumericSuffix::Mm,
15050                }),
15051            },
15052            construction: None,
15053        };
15054        let segments = vec![ExistingSegmentCtor {
15055            id: line_id,
15056            ctor: SegmentCtor::Line(line_ctor),
15057        }];
15058        let (src_delta, scene_delta) = frontend
15059            .edit_segments(&mock_ctx, version, sketch_id, segments)
15060            .await
15061            .unwrap();
15062        assert!(
15063            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15064            "Expected edited line in source, got: {}",
15065            src_delta.text
15066        );
15067        assert_eq!(scene_delta.new_objects, vec![]);
15068
15069        ctx.close().await;
15070        mock_ctx.close().await;
15071    }
15072}