Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::SegmentDragAnchor;
19use crate::collections::AhashIndexSet;
20use crate::execution::Artifact;
21use crate::execution::ArtifactGraph;
22use crate::execution::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::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::parsing::ast::types::NodePathExt;
90use crate::pretty::NumericSuffix;
91use crate::std::constraints::LinesAtAngleKind;
92use crate::walk::NodeMut;
93use crate::walk::Visitable;
94
95pub(crate) mod api;
96pub(crate) mod modify;
97pub(crate) mod sketch;
98
99pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
100
101#[derive(Debug, Clone)]
102struct SketchCheckpoint {
103    id: SketchCheckpointId,
104    source: SourceDelta,
105    program: Program,
106    scene_graph: SceneGraph,
107    exec_outcome: ExecOutcome,
108    point_freedom_cache: HashMap<ObjectId, Freedom>,
109    mock_memory: Option<SketchModeState>,
110}
111mod traverse;
112pub(crate) mod trim;
113
114struct ArcSizeConstraintParams {
115    points: Vec<ObjectId>,
116    function_name: &'static str,
117    value: f64,
118    units: NumericSuffix,
119    label_position: Option<Point2d<Number>>,
120    constraint_type_name: &'static str,
121}
122
123const POINT_FN: &str = "point";
124const POINT_AT_PARAM: &str = "at";
125const LINE_FN: &str = "line";
126const LINE_VARIABLE: &str = "line";
127const LINE_START_PARAM: &str = "start";
128const LINE_END_PARAM: &str = "end";
129const ARC_FN: &str = "arc";
130const ARC_VARIABLE: &str = "arc";
131const ARC_START_PARAM: &str = "start";
132const ARC_END_PARAM: &str = "end";
133const ARC_CENTER_PARAM: &str = "center";
134const CIRCLE_FN: &str = "circle";
135const CIRCLE_VARIABLE: &str = "circle";
136const CIRCLE_START_PARAM: &str = "start";
137const CIRCLE_CENTER_PARAM: &str = "center";
138const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
139const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
140const LABEL_POSITION_PARAM: &str = "labelPosition";
141
142const COINCIDENT_FN: &str = "coincident";
143const DIAMETER_FN: &str = "diameter";
144const DISTANCE_FN: &str = "distance";
145const FIXED_FN: &str = "fixed";
146const ANGLE_FN: &str = "angle";
147const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
148const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
149const EQUAL_LENGTH_FN: &str = "equalLength";
150const EQUAL_RADIUS_FN: &str = "equalRadius";
151const HORIZONTAL_FN: &str = "horizontal";
152const MIDPOINT_FN: &str = "midpoint";
153const MIDPOINT_POINT_PARAM: &str = "point";
154const RADIUS_FN: &str = "radius";
155const SYMMETRIC_FN: &str = "symmetric";
156const SYMMETRIC_AXIS_PARAM: &str = "axis";
157const TANGENT_FN: &str = "tangent";
158const VERTICAL_FN: &str = "vertical";
159
160const LINE_PROPERTY_START: &str = "start";
161const LINE_PROPERTY_END: &str = "end";
162
163const ARC_PROPERTY_START: &str = "start";
164const ARC_PROPERTY_END: &str = "end";
165const ARC_PROPERTY_CENTER: &str = "center";
166const CIRCLE_PROPERTY_START: &str = "start";
167const CIRCLE_PROPERTY_CENTER: &str = "center";
168const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
169const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
170
171const CONSTRUCTION_PARAM: &str = "construction";
172
173#[derive(Debug, Clone, Copy)]
174enum EditDeleteKind {
175    Edit,
176    DeleteNonSketch,
177}
178
179/// Options that control how an edit is re-executed and written back.
180struct ExecuteAfterEditOptions {
181    segment_ids_edited: AhashIndexSet<ObjectId>,
182    edit_kind: EditDeleteKind,
183    commit_solved_initial_guesses: bool,
184}
185
186impl EditDeleteKind {
187    /// Returns true if this edit is any type of deletion.
188    fn is_delete(&self) -> bool {
189        match self {
190            EditDeleteKind::Edit => false,
191            EditDeleteKind::DeleteNonSketch => true,
192        }
193    }
194
195    fn to_change_kind(self) -> ChangeKind {
196        match self {
197            EditDeleteKind::Edit => ChangeKind::Edit,
198            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
199        }
200    }
201}
202
203#[derive(Debug, Clone, Copy)]
204enum ChangeKind {
205    Add,
206    Edit,
207    Delete,
208    None,
209}
210
211#[derive(Debug, Clone, Serialize, ts_rs::TS)]
212#[ts(export, export_to = "FrontendApi.ts")]
213#[serde(tag = "type")]
214pub enum SetProgramOutcome {
215    #[serde(rename_all = "camelCase")]
216    Success {
217        scene_graph: Box<SceneGraph>,
218        exec_outcome: Box<ExecOutcome>,
219        checkpoint_id: Option<SketchCheckpointId>,
220    },
221    #[serde(rename_all = "camelCase")]
222    ExecFailure { error: Box<KclErrorWithOutputs> },
223}
224
225/// Options for a sketch segment edit that participates in drag solving.
226pub struct EditSegmentsOptions {
227    /// Narrows which edited scene objects receive temporary fixed constraints.
228    ///
229    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
230    /// disables those fixed constraints, which is useful for semantic edits such
231    /// as toggling construction state.
232    pub anchor_segment_ids: Option<Vec<ObjectId>>,
233    /// Hidden fixed cursor points that the referenced segment bodies must pass
234    /// through during solve.
235    pub drag_anchors: Vec<SegmentDragAnchor>,
236    /// Whether solver-updated initial guesses should be written back to KCL.
237    pub commit_solved_initial_guesses: bool,
238}
239
240/// Options for a distance-constraint label edit during sketch dragging.
241pub struct EditDistanceConstraintLabelPositionOptions {
242    /// Edited scene objects to keep anchored while previewing the label edit.
243    pub anchor_segment_ids: Vec<ObjectId>,
244    /// Whether solver-updated initial guesses should be written back to KCL.
245    pub commit_solved_initial_guesses: bool,
246}
247
248#[derive(Debug, Clone)]
249pub struct FrontendState {
250    program: Program,
251    scene_graph: SceneGraph,
252    /// Stores the last known freedom value for each point object.
253    /// This allows us to preserve freedom values when freedom analysis isn't run.
254    point_freedom_cache: HashMap<ObjectId, Freedom>,
255    /// One-shot drag anchors for the next segment edit. These ids define which
256    /// edited points/segments become temporary fixed constraints during solve.
257    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
258    /// One-shot segment-body drag anchors for the next segment edit. These add
259    /// a temporary solver point on the dragged segment that follows the cursor.
260    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
261    /// One-shot override for whether the next edit commits solver-updated
262    /// initial guesses back into KCL. Drag previews keep this off so only the
263    /// explicit drag edit feeds the next solve.
264    next_edit_commits_solver_solutions: Option<bool>,
265    sketch_checkpoints: VecDeque<SketchCheckpoint>,
266    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
267}
268
269impl Default for FrontendState {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275impl FrontendState {
276    pub fn new() -> Self {
277        Self {
278            program: Program::empty(),
279            scene_graph: SceneGraph {
280                project: ProjectId(0),
281                file: FileId(0),
282                version: Version(0),
283                objects: Default::default(),
284                settings: Default::default(),
285                sketch_mode: Default::default(),
286            },
287            point_freedom_cache: HashMap::new(),
288            next_drag_anchor_segment_ids: None,
289            next_segment_drag_anchors: None,
290            next_edit_commits_solver_solutions: None,
291            sketch_checkpoints: VecDeque::new(),
292            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
293        }
294    }
295
296    /// Get a reference to the scene graph
297    pub fn scene_graph(&self) -> &SceneGraph {
298        &self.scene_graph
299    }
300
301    pub fn default_length_unit(&self) -> UnitLength {
302        self.program
303            .meta_settings()
304            .ok()
305            .flatten()
306            .map(|settings| settings.default_length_units)
307            .unwrap_or(UnitLength::Millimeters)
308    }
309
310    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
311        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
312
313        let checkpoint = SketchCheckpoint {
314            id: checkpoint_id,
315            source: SourceDelta {
316                text: source_from_ast(&self.program.ast),
317            },
318            program: self.program.clone(),
319            scene_graph: self.scene_graph.clone(),
320            exec_outcome,
321            point_freedom_cache: self.point_freedom_cache.clone(),
322            mock_memory: read_old_memory().await,
323        };
324
325        self.sketch_checkpoints.push_back(checkpoint);
326        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
327            self.sketch_checkpoints.pop_front();
328        }
329
330        Ok(checkpoint_id)
331    }
332
333    /// Edit sketch segments with optional drag-solve overrides.
334    ///
335    /// Drag anchors add hidden fixed cursor points and constrain the referenced
336    /// segment bodies to pass through them, which lets body drags use the same
337    /// anchor model without pinning all child points. Preview callers disable
338    /// solver writeback so solved geometry can be returned without feeding every
339    /// solver value back into KCL.
340    pub async fn edit_segments_with_options(
341        &mut self,
342        ctx: &ExecutorContext,
343        version: Version,
344        sketch: ObjectId,
345        segments: Vec<ExistingSegmentCtor>,
346        options: EditSegmentsOptions,
347    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
348        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
349            self.next_drag_anchor_segment_ids
350                .replace(anchor_ids.into_iter().collect())
351        });
352        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
353        let previous_commit_mode = self
354            .next_edit_commits_solver_solutions
355            .replace(options.commit_solved_initial_guesses);
356        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
357        if let Some(previous_anchor_ids) = previous_anchor_ids {
358            self.next_drag_anchor_segment_ids = previous_anchor_ids;
359        }
360        self.next_segment_drag_anchors = previous_drag_anchors;
361        self.next_edit_commits_solver_solutions = previous_commit_mode;
362        result
363    }
364
365    /// Edit a distance-constraint label position with optional solver writeback.
366    ///
367    /// Drag previews set `commit_solved_initial_guesses` to false so label
368    /// placement can be previewed against solved geometry without advancing
369    /// persistent KCL state until drag completion.
370    pub async fn edit_distance_constraint_label_position_with_options(
371        &mut self,
372        ctx: &ExecutorContext,
373        version: Version,
374        sketch: ObjectId,
375        constraint_id: ObjectId,
376        label_position: Point2d<Number>,
377        options: EditDistanceConstraintLabelPositionOptions,
378    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
379        let previous_commit_mode = self
380            .next_edit_commits_solver_solutions
381            .replace(options.commit_solved_initial_guesses);
382        let result = SketchApi::edit_distance_constraint_label_position(
383            self,
384            ctx,
385            version,
386            sketch,
387            constraint_id,
388            label_position,
389            options.anchor_segment_ids,
390        )
391        .await;
392        self.next_edit_commits_solver_solutions = previous_commit_mode;
393        result
394    }
395
396    pub async fn restore_sketch_checkpoint(
397        &mut self,
398        checkpoint_id: SketchCheckpointId,
399    ) -> api::Result<RestoreSketchCheckpointOutcome> {
400        let checkpoint = self
401            .sketch_checkpoints
402            .iter()
403            .find(|checkpoint| checkpoint.id == checkpoint_id)
404            .cloned()
405            .ok_or_else(|| Error {
406                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
407            })?;
408
409        self.program = checkpoint.program;
410        self.scene_graph = checkpoint.scene_graph.clone();
411        self.point_freedom_cache = checkpoint.point_freedom_cache;
412        self.next_drag_anchor_segment_ids = None;
413        self.next_segment_drag_anchors = None;
414        self.next_edit_commits_solver_solutions = None;
415
416        if let Some(mock_memory) = checkpoint.mock_memory {
417            write_old_memory(mock_memory).await;
418        } else {
419            clear_mem_cache().await;
420        }
421
422        Ok(RestoreSketchCheckpointOutcome {
423            source_delta: checkpoint.source,
424            scene_graph_delta: SceneGraphDelta {
425                new_graph: self.scene_graph_for_ui(),
426                new_objects: Vec::new(),
427                invalidates_ids: true,
428                exec_outcome: checkpoint.exec_outcome,
429            },
430        })
431    }
432
433    pub fn clear_sketch_checkpoints(&mut self) {
434        self.sketch_checkpoints.clear();
435    }
436    fn scene_graph_for_ui(&self) -> SceneGraph {
437        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
438            matches!(
439                object.kind,
440                ObjectKind::Segment {
441                    segment: Segment::ControlPointSpline(_)
442                }
443            )
444        });
445
446        if !has_control_point_splines {
447            return self.scene_graph.clone();
448        }
449
450        let hidden_constraint_ids = self
451            .scene_graph
452            .objects
453            .iter()
454            .filter_map(|object| match &object.kind {
455                ObjectKind::Constraint {
456                    constraint: Constraint::Coincident(coincident),
457                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
458                    Some(object.id)
459                }
460                _ => None,
461            })
462            .collect::<HashSet<_>>();
463
464        if hidden_constraint_ids.is_empty() {
465            return self.scene_graph.clone();
466        }
467
468        let mut scene_graph = self.scene_graph.clone();
469        for object in &mut scene_graph.objects {
470            match &mut object.kind {
471                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
472                    object.kind = ObjectKind::Nil;
473                }
474                ObjectKind::Sketch(sketch) => {
475                    sketch
476                        .constraints
477                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
478                }
479                _ => {}
480            }
481        }
482
483        scene_graph
484    }
485}
486
487fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
488    let mut first_owner_id = None;
489    for segment_id in coincident.segment_ids() {
490        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
491            return false;
492        };
493
494        match first_owner_id {
495            Some(first_owner_id) if first_owner_id != owner_id => return false,
496            Some(_) => {}
497            None => first_owner_id = Some(owner_id),
498        }
499    }
500
501    first_owner_id.is_some()
502}
503
504fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
505    let object = scene_graph.objects.get(segment_id.0)?;
506    let ObjectKind::Segment { segment } = &object.kind else {
507        return None;
508    };
509
510    match segment {
511        Segment::ControlPointSpline(_) => Some(segment_id),
512        Segment::Point(point) => point
513            .owner
514            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
515        Segment::Line(line) => line
516            .owner
517            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
518        _ => None,
519    }
520}
521
522fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
523    matches!(
524        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
525        Some(ObjectKind::Segment {
526            segment: Segment::ControlPointSpline(_)
527        })
528    )
529}
530
531fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
532    let experimental_features_allowed = program
533        .meta_settings()
534        .ok()
535        .flatten()
536        .map(|settings| settings.experimental_features == WarningLevel::Allow)
537        .unwrap_or(false);
538    if experimental_features_allowed {
539        return Ok(program.clone());
540    }
541
542    program.change_experimental_features(Some(WarningLevel::Allow))
543}
544
545impl SketchApi for FrontendState {
546    async fn execute_mock(
547        &mut self,
548        ctx: &ExecutorContext,
549        _version: Version,
550        sketch: ObjectId,
551    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
552        let sketch_block_ref =
553            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
554
555        let mut truncated_program = self.program.clone();
556        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
557            .map_err(KclErrorWithOutputs::no_outputs)?;
558
559        // Execute.
560        let outcome = ctx
561            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
562            .await?;
563        let new_source = source_from_ast(&self.program.ast);
564        let src_delta = SourceDelta { text: new_source };
565        // MockConfig::default() has freedom_analysis: true
566        let outcome = self.update_state_after_exec(outcome, true);
567        let scene_graph_delta = SceneGraphDelta {
568            new_graph: self.scene_graph.clone(),
569            new_objects: Default::default(),
570            invalidates_ids: false,
571            exec_outcome: outcome,
572        };
573        Ok((src_delta, scene_graph_delta))
574    }
575
576    async fn new_sketch(
577        &mut self,
578        ctx: &ExecutorContext,
579        _project: ProjectId,
580        _file: FileId,
581        _version: Version,
582        args: SketchCtor,
583    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
584        // TODO: Check version.
585
586        let mut new_ast = self.program.ast.clone();
587        // Create updated KCL source from args.
588        let mut plane_ast =
589            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
590        let mut defined_names = find_defined_names(&new_ast);
591        let is_face_of_expr = matches!(
592            &plane_ast,
593            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
594        );
595        if is_face_of_expr {
596            let face_name = next_free_name_with_padding("face", &defined_names)
597                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
598            let face_decl = ast::VariableDeclaration::new(
599                ast::VariableDeclarator::new(&face_name, plane_ast),
600                ast::ItemVisibility::Default,
601                ast::VariableKind::Const,
602            );
603            new_ast
604                .body
605                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
606                    face_decl,
607                ))));
608            defined_names.insert(face_name.clone());
609            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
610        }
611        let sketch_ast = ast::SketchBlock {
612            arguments: vec![ast::LabeledArg {
613                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
614                arg: plane_ast,
615            }],
616            body: Default::default(),
617            is_being_edited: false,
618            non_code_meta: Default::default(),
619            digest: None,
620        };
621        // Add a sketch block as a variable declaration directly, avoiding
622        // source-range mutation on a no-src node.
623        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
624            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
625        let sketch_decl = ast::VariableDeclaration::new(
626            ast::VariableDeclarator::new(
627                &sketch_name,
628                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
629            ),
630            ast::ItemVisibility::Default,
631            ast::VariableKind::Const,
632        );
633        new_ast
634            .body
635            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
636                sketch_decl,
637            ))));
638        // Convert to string source to create real source ranges.
639        let new_source = source_from_ast(&new_ast);
640        // Parse the new source.
641        let (new_program, errors) = Program::parse(&new_source)
642            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
643        if !errors.is_empty() {
644            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
645                "Error parsing KCL source after adding sketch: {errors:?}"
646            ))));
647        }
648        let Some(new_program) = new_program else {
649            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
650                "No AST produced after adding sketch".to_owned(),
651            )));
652        };
653
654        // Make sure to only set this if there are no errors.
655        self.program = new_program.clone();
656
657        // We need to do an engine execute so that the plane object gets created
658        // and is cached.
659        let outcome = ctx.run_with_caching(new_program.clone()).await?;
660        let freedom_analysis_ran = true;
661
662        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
663
664        let Some(sketch_id) = self
665            .scene_graph
666            .objects
667            .iter()
668            .filter_map(|object| match object.kind {
669                ObjectKind::Sketch(_) => Some(object.id),
670                _ => None,
671            })
672            .max_by_key(|id| id.0)
673        else {
674            return Err(KclErrorWithOutputs::from_error_outcome(
675                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
676                outcome,
677            ));
678        };
679        // Store the object in the scene.
680        self.scene_graph.sketch_mode = Some(sketch_id);
681
682        let src_delta = SourceDelta { text: new_source };
683        let scene_graph_delta = SceneGraphDelta {
684            new_graph: self.scene_graph_for_ui(),
685            invalidates_ids: false,
686            new_objects: vec![sketch_id],
687            exec_outcome: outcome,
688        };
689        Ok((src_delta, scene_graph_delta, sketch_id))
690    }
691
692    async fn edit_sketch(
693        &mut self,
694        ctx: &ExecutorContext,
695        _project: ProjectId,
696        _file: FileId,
697        _version: Version,
698        sketch: ObjectId,
699    ) -> ExecResult<SceneGraphDelta> {
700        // TODO: Check version.
701
702        // Look up existing sketch.
703        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
704            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
705        })?;
706        let ObjectKind::Sketch(_) = &sketch_object.kind else {
707            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
708                "Object is not a sketch, it is {}",
709                sketch_object.kind.human_friendly_kind_with_article()
710            ))));
711        };
712        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
713
714        // Enter sketch mode by setting the sketch_mode.
715        self.scene_graph.sketch_mode = Some(sketch);
716
717        // Truncate after the sketch block for mock execution.
718        let mut truncated_program = self.program.clone();
719        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
720            .map_err(KclErrorWithOutputs::no_outputs)?;
721
722        // Execute in mock mode to ensure state is up to date. The caller will
723        // want freedom analysis to display segments correctly.
724        let outcome = ctx
725            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
726            .await?;
727
728        // MockConfig::default() has freedom_analysis: true
729        let outcome = self.update_state_after_exec(outcome, true);
730        let scene_graph_delta = SceneGraphDelta {
731            new_graph: self.scene_graph_for_ui(),
732            invalidates_ids: false,
733            new_objects: Vec::new(),
734            exec_outcome: outcome,
735        };
736        Ok(scene_graph_delta)
737    }
738
739    async fn exit_sketch(
740        &mut self,
741        ctx: &ExecutorContext,
742        _version: Version,
743        sketch: ObjectId,
744    ) -> ExecResult<SceneGraph> {
745        // TODO: Check version.
746        #[cfg(not(target_arch = "wasm32"))]
747        let _ = sketch;
748        #[cfg(target_arch = "wasm32")]
749        if self.scene_graph.sketch_mode != Some(sketch) {
750            web_sys::console::warn_1(
751                &format!(
752                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
753                    &self.scene_graph.sketch_mode
754                )
755                .into(),
756            );
757        }
758        self.scene_graph.sketch_mode = None;
759
760        // Execute.
761        let outcome = ctx.run_with_caching(self.program.clone()).await?;
762
763        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
764        self.update_state_after_exec(outcome, false);
765
766        Ok(self.scene_graph_for_ui())
767    }
768
769    async fn delete_sketch(
770        &mut self,
771        ctx: &ExecutorContext,
772        _version: Version,
773        sketch: ObjectId,
774    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
775        // TODO: Check version.
776
777        let mut new_ast = self.program.ast.clone();
778
779        // Look up existing sketch.
780        let sketch_id = sketch;
781        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
782            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
783        })?;
784        let ObjectKind::Sketch(_) = &sketch_object.kind else {
785            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
786                "Object is not a sketch, it is {}",
787                sketch_object.kind.human_friendly_kind_with_article(),
788            ))));
789        };
790
791        // Modify the AST to remove the sketch.
792        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
793            .map_err(KclErrorWithOutputs::no_outputs)?;
794
795        self.execute_after_delete_sketch(ctx, &mut new_ast).await
796    }
797
798    async fn add_segment(
799        &mut self,
800        ctx: &ExecutorContext,
801        _version: Version,
802        sketch: ObjectId,
803        segment: SegmentCtor,
804        _label: Option<String>,
805    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
806        // TODO: Check version.
807        match segment {
808            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
809            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
810            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
811            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
812            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
813        }
814    }
815
816    async fn edit_segments(
817        &mut self,
818        ctx: &ExecutorContext,
819        _version: Version,
820        sketch: ObjectId,
821        segments: Vec<ExistingSegmentCtor>,
822    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
823        // TODO: Check version.
824        let sketch_block_ref =
825            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
826
827        let mut new_ast = self.program.ast.clone();
828        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
829        let mut invalidates_ids = false;
830
831        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
832        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
833        for segment in &segments {
834            edited_segment_ids.insert(segment.id);
835            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
836                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
837                && let ObjectKind::Segment {
838                    segment: Segment::ControlPointSpline(existing_spline),
839                } = &existing_object.kind
840                && existing_spline.controls.len() != new_ctor.points.len()
841            {
842                invalidates_ids = true;
843            }
844        }
845        let drag_anchor_segment_ids = self
846            .next_drag_anchor_segment_ids
847            .take()
848            .unwrap_or_else(|| edited_segment_ids.clone());
849        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
850
851        // Preprocess segments into a final_edits vector to handle if segments contains:
852        // - edit start point of line1 (as SegmentCtor::Point)
853        // - edit end point of line1 (as SegmentCtor::Point)
854        //
855        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
856        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
857        //
858        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
859        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
860        // so the above example would result in a single line1 edit:
861        // - the first start point edit creates a new line edit entry in final_edits
862        // - the second end point edit finds this line edit and mutates the end position only.
863        //
864        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
865        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
866
867        for segment in segments {
868            let segment_id = segment.id;
869            match segment.ctor {
870                SegmentCtor::Point(ctor) => {
871                    // Find the owner, if any (point -> line / arc)
872                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
873                        && let ObjectKind::Segment { segment } = &segment_object.kind
874                        && let Segment::Point(point) = segment
875                        && let Some(owner_id) = point.owner
876                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
877                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
878                    {
879                        match owner_segment {
880                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
881                                if let Some(existing) = final_edits.get_mut(&owner_id) {
882                                    let SegmentCtor::Line(line_ctor) = existing else {
883                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
884                                            "Internal: Expected line ctor for owner, but found {}",
885                                            existing.human_friendly_kind_with_article()
886                                        ))));
887                                    };
888                                    // Line owner is already in final_edits -> apply this point edit
889                                    if line.start == segment_id {
890                                        line_ctor.start = ctor.position;
891                                    } else {
892                                        line_ctor.end = ctor.position;
893                                    }
894                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
895                                    // Line owner is not in final_edits yet -> create it
896                                    let mut line_ctor = line_ctor.clone();
897                                    if line.start == segment_id {
898                                        line_ctor.start = ctor.position;
899                                    } else {
900                                        line_ctor.end = ctor.position;
901                                    }
902                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
903                                } else {
904                                    // This should never run..
905                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
906                                        "Internal: Line does not have line ctor, but found {}",
907                                        line.ctor.human_friendly_kind_with_article()
908                                    ))));
909                                }
910                                continue;
911                            }
912                            Segment::Arc(arc)
913                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
914                            {
915                                if let Some(existing) = final_edits.get_mut(&owner_id) {
916                                    let SegmentCtor::Arc(arc_ctor) = existing else {
917                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
918                                            "Internal: Expected arc ctor for owner, but found {}",
919                                            existing.human_friendly_kind_with_article()
920                                        ))));
921                                    };
922                                    if arc.start == segment_id {
923                                        arc_ctor.start = ctor.position;
924                                    } else if arc.end == segment_id {
925                                        arc_ctor.end = ctor.position;
926                                    } else {
927                                        arc_ctor.center = ctor.position;
928                                    }
929                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
930                                    let mut arc_ctor = arc_ctor.clone();
931                                    if arc.start == segment_id {
932                                        arc_ctor.start = ctor.position;
933                                    } else if arc.end == segment_id {
934                                        arc_ctor.end = ctor.position;
935                                    } else {
936                                        arc_ctor.center = ctor.position;
937                                    }
938                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
939                                } else {
940                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
941                                        "Internal: Arc does not have arc ctor, but found {}",
942                                        arc.ctor.human_friendly_kind_with_article()
943                                    ))));
944                                }
945                                continue;
946                            }
947                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
948                                if let Some(existing) = final_edits.get_mut(&owner_id) {
949                                    let SegmentCtor::Circle(circle_ctor) = existing else {
950                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
951                                            "Internal: Expected circle ctor for owner, but found {}",
952                                            existing.human_friendly_kind_with_article()
953                                        ))));
954                                    };
955                                    if circle.start == segment_id {
956                                        circle_ctor.start = ctor.position;
957                                    } else {
958                                        circle_ctor.center = ctor.position;
959                                    }
960                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
961                                    let mut circle_ctor = circle_ctor.clone();
962                                    if circle.start == segment_id {
963                                        circle_ctor.start = ctor.position;
964                                    } else {
965                                        circle_ctor.center = ctor.position;
966                                    }
967                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
968                                } else {
969                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
970                                        "Internal: Circle does not have circle ctor, but found {}",
971                                        circle.ctor.human_friendly_kind_with_article()
972                                    ))));
973                                }
974                                continue;
975                            }
976                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
977                                let Some(control_index) =
978                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
979                                else {
980                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
981                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
982                                    ))));
983                                };
984                                if let Some(existing) = final_edits.get_mut(&owner_id) {
985                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
986                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
987                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
988                                            existing.human_friendly_kind_with_article()
989                                        ))));
990                                    };
991                                    spline_ctor.points[control_index] = ctor.position;
992                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
993                                    let mut spline_ctor = spline_ctor.clone();
994                                    spline_ctor.points[control_index] = ctor.position;
995                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
996                                } else {
997                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
998                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
999                                        spline.ctor.human_friendly_kind_with_article()
1000                                    ))));
1001                                }
1002                                continue;
1003                            }
1004                            _ => {}
1005                        }
1006                    }
1007
1008                    // No owner, it's an individual point
1009                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1010                }
1011                SegmentCtor::Line(ctor) => {
1012                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1013                }
1014                SegmentCtor::Arc(ctor) => {
1015                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1016                }
1017                SegmentCtor::Circle(ctor) => {
1018                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1019                }
1020                SegmentCtor::ControlPointSpline(ctor) => {
1021                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1022                }
1023            }
1024        }
1025
1026        for (segment_id, ctor) in final_edits {
1027            match ctor {
1028                SegmentCtor::Point(ctor) => self
1029                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1030                    .map_err(KclErrorWithOutputs::no_outputs)?,
1031                SegmentCtor::Line(ctor) => self
1032                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1033                    .map_err(KclErrorWithOutputs::no_outputs)?,
1034                SegmentCtor::Arc(ctor) => self
1035                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1036                    .map_err(KclErrorWithOutputs::no_outputs)?,
1037                SegmentCtor::Circle(ctor) => self
1038                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1039                    .map_err(KclErrorWithOutputs::no_outputs)?,
1040                SegmentCtor::ControlPointSpline(ctor) => self
1041                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1042                    .map_err(KclErrorWithOutputs::no_outputs)?,
1043            }
1044        }
1045        let (source_delta, mut scene_graph_delta) = self
1046            .execute_after_edit(
1047                ctx,
1048                sketch,
1049                sketch_block_ref,
1050                &mut new_ast,
1051                ExecuteAfterEditOptions {
1052                    segment_ids_edited: drag_anchor_segment_ids,
1053                    edit_kind: EditDeleteKind::Edit,
1054                    commit_solved_initial_guesses,
1055                },
1056            )
1057            .await?;
1058        if invalidates_ids {
1059            scene_graph_delta.invalidates_ids = true;
1060        }
1061        Ok((source_delta, scene_graph_delta))
1062    }
1063
1064    async fn delete_objects(
1065        &mut self,
1066        ctx: &ExecutorContext,
1067        _version: Version,
1068        sketch: ObjectId,
1069        constraint_ids: Vec<ObjectId>,
1070        segment_ids: Vec<ObjectId>,
1071    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1072        // TODO: Check version.
1073        let sketch_block_ref =
1074            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1075
1076        // Deduplicate IDs.
1077        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1078        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1079
1080        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1081        // also delete the point, as well as other points that are owned by the owner.
1082        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1083
1084        for segment_id in segment_ids_set.iter().copied() {
1085            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1086                let ObjectKind::Segment { segment } = &segment_object.kind else {
1087                    return None;
1088                };
1089                match segment {
1090                    Segment::Point(point) => point.owner,
1091                    Segment::Line(line) => line.owner,
1092                    _ => None,
1093                }
1094            });
1095
1096            if let Some(owner_id) = owner_id
1097                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1098                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1099                && matches!(
1100                    owner_segment,
1101                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1102                )
1103            {
1104                // segment is owned -> delete the owner
1105                resolved_segment_ids_to_delete.insert(owner_id);
1106            } else {
1107                // segment is not owned by anything -> can be deleted
1108                resolved_segment_ids_to_delete.insert(segment_id);
1109            }
1110        }
1111        let referenced_constraint_ids = self
1112            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1113            .map_err(KclErrorWithOutputs::no_outputs)?;
1114
1115        let mut new_ast = self.program.ast.clone();
1116
1117        for constraint_id in referenced_constraint_ids {
1118            if constraint_ids_set.contains(&constraint_id) {
1119                continue;
1120            }
1121
1122            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1123                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1124            })?;
1125            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1126                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1127                    "Object is not a constraint, it is {}",
1128                    constraint_object.kind.human_friendly_kind_with_article()
1129                ))));
1130            };
1131
1132            match constraint {
1133                Constraint::Coincident(coincident) => {
1134                    let remaining_segments =
1135                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1136
1137                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1138                    if remaining_segments.len() >= 2 {
1139                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1140                            .map_err(KclErrorWithOutputs::no_outputs)?;
1141                    } else {
1142                        constraint_ids_set.insert(constraint_id);
1143                    }
1144                }
1145                Constraint::EqualRadius(equal_radius) => {
1146                    let remaining_input = equal_radius
1147                        .input
1148                        .iter()
1149                        .copied()
1150                        .filter(|segment_id| {
1151                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1152                        })
1153                        .collect::<Vec<_>>();
1154
1155                    if remaining_input.len() >= 2 {
1156                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1157                            .map_err(KclErrorWithOutputs::no_outputs)?;
1158                    } else {
1159                        constraint_ids_set.insert(constraint_id);
1160                    }
1161                }
1162                Constraint::LinesEqualLength(lines_equal_length) => {
1163                    let remaining_lines = lines_equal_length
1164                        .lines
1165                        .iter()
1166                        .copied()
1167                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1168                        .collect::<Vec<_>>();
1169
1170                    // Equal length constraint is only valid with at least 2 lines
1171                    if remaining_lines.len() >= 2 {
1172                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1173                            .map_err(KclErrorWithOutputs::no_outputs)?;
1174                    } else {
1175                        constraint_ids_set.insert(constraint_id);
1176                    }
1177                }
1178                Constraint::Parallel(parallel) => {
1179                    let remaining_lines = parallel
1180                        .lines
1181                        .iter()
1182                        .copied()
1183                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1184                        .collect::<Vec<_>>();
1185
1186                    if remaining_lines.len() >= 2 {
1187                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1188                            .map_err(KclErrorWithOutputs::no_outputs)?;
1189                    } else {
1190                        constraint_ids_set.insert(constraint_id);
1191                    }
1192                }
1193                Constraint::Horizontal(Horizontal::Points { points }) => {
1194                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1195
1196                    if remaining_points.len() >= 2 {
1197                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1198                            .map_err(KclErrorWithOutputs::no_outputs)?;
1199                    } else {
1200                        constraint_ids_set.insert(constraint_id);
1201                    }
1202                }
1203                Constraint::Vertical(Vertical::Points { points }) => {
1204                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1205
1206                    if remaining_points.len() >= 2 {
1207                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1208                            .map_err(KclErrorWithOutputs::no_outputs)?;
1209                    } else {
1210                        constraint_ids_set.insert(constraint_id);
1211                    }
1212                }
1213                Constraint::Fixed(fixed) => {
1214                    if fixed.points.iter().any(|fixed_point| {
1215                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1216                    }) {
1217                        constraint_ids_set.insert(constraint_id);
1218                    }
1219                }
1220                _ => {
1221                    // All other constraint types: if referenced by a segment -> delete the constraint
1222                    constraint_ids_set.insert(constraint_id);
1223                }
1224            }
1225        }
1226
1227        for constraint_id in constraint_ids_set {
1228            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1229                .map_err(KclErrorWithOutputs::no_outputs)?;
1230        }
1231        for segment_id in resolved_segment_ids_to_delete {
1232            self.delete_segment(&mut new_ast, sketch, segment_id)
1233                .map_err(KclErrorWithOutputs::no_outputs)?;
1234        }
1235
1236        self.execute_after_edit(
1237            ctx,
1238            sketch,
1239            sketch_block_ref,
1240            &mut new_ast,
1241            ExecuteAfterEditOptions {
1242                segment_ids_edited: Default::default(),
1243                edit_kind: EditDeleteKind::DeleteNonSketch,
1244                commit_solved_initial_guesses: true,
1245            },
1246        )
1247        .await
1248    }
1249
1250    async fn add_constraint(
1251        &mut self,
1252        ctx: &ExecutorContext,
1253        _version: Version,
1254        sketch: ObjectId,
1255        constraint: Constraint,
1256    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1257        // TODO: Check version.
1258
1259        // Save the original state as a backup - we'll restore it if anything fails
1260        let original_program = self.program.clone();
1261        let original_scene_graph = self.scene_graph.clone();
1262
1263        let mut new_ast = self.program.ast.clone();
1264        let sketch_block_ref = match constraint {
1265            Constraint::Coincident(coincident) => self
1266                .add_coincident(sketch, coincident, &mut new_ast)
1267                .await
1268                .map_err(KclErrorWithOutputs::no_outputs)?,
1269            Constraint::Distance(distance) => self
1270                .add_distance(sketch, distance, &mut new_ast)
1271                .await
1272                .map_err(KclErrorWithOutputs::no_outputs)?,
1273            Constraint::EqualRadius(equal_radius) => self
1274                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1275                .await
1276                .map_err(KclErrorWithOutputs::no_outputs)?,
1277            Constraint::Fixed(fixed) => self
1278                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1279                .await
1280                .map_err(KclErrorWithOutputs::no_outputs)?,
1281            Constraint::HorizontalDistance(distance) => self
1282                .add_horizontal_distance(sketch, distance, &mut new_ast)
1283                .await
1284                .map_err(KclErrorWithOutputs::no_outputs)?,
1285            Constraint::VerticalDistance(distance) => self
1286                .add_vertical_distance(sketch, distance, &mut new_ast)
1287                .await
1288                .map_err(KclErrorWithOutputs::no_outputs)?,
1289            Constraint::Horizontal(horizontal) => self
1290                .add_horizontal(sketch, horizontal, &mut new_ast)
1291                .await
1292                .map_err(KclErrorWithOutputs::no_outputs)?,
1293            Constraint::LinesEqualLength(lines_equal_length) => self
1294                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1295                .await
1296                .map_err(KclErrorWithOutputs::no_outputs)?,
1297            Constraint::Midpoint(midpoint) => self
1298                .add_midpoint(sketch, midpoint, &mut new_ast)
1299                .await
1300                .map_err(KclErrorWithOutputs::no_outputs)?,
1301            Constraint::Parallel(parallel) => self
1302                .add_parallel(sketch, parallel, &mut new_ast)
1303                .await
1304                .map_err(KclErrorWithOutputs::no_outputs)?,
1305            Constraint::Perpendicular(perpendicular) => self
1306                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1307                .await
1308                .map_err(KclErrorWithOutputs::no_outputs)?,
1309            Constraint::Radius(radius) => self
1310                .add_radius(sketch, radius, &mut new_ast)
1311                .await
1312                .map_err(KclErrorWithOutputs::no_outputs)?,
1313            Constraint::Diameter(diameter) => self
1314                .add_diameter(sketch, diameter, &mut new_ast)
1315                .await
1316                .map_err(KclErrorWithOutputs::no_outputs)?,
1317            Constraint::Symmetric(symmetric) => self
1318                .add_symmetric(sketch, symmetric, &mut new_ast)
1319                .await
1320                .map_err(KclErrorWithOutputs::no_outputs)?,
1321            Constraint::Vertical(vertical) => self
1322                .add_vertical(sketch, vertical, &mut new_ast)
1323                .await
1324                .map_err(KclErrorWithOutputs::no_outputs)?,
1325            Constraint::Angle(lines_at_angle) => self
1326                .add_angle(sketch, lines_at_angle, &mut new_ast)
1327                .await
1328                .map_err(KclErrorWithOutputs::no_outputs)?,
1329            Constraint::Tangent(tangent) => self
1330                .add_tangent(sketch, tangent, &mut new_ast)
1331                .await
1332                .map_err(KclErrorWithOutputs::no_outputs)?,
1333        };
1334
1335        let result = self
1336            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1337            .await;
1338
1339        // If execution failed, restore the original state to prevent corruption
1340        if result.is_err() {
1341            self.program = original_program;
1342            self.scene_graph = original_scene_graph;
1343        }
1344
1345        result
1346    }
1347
1348    async fn chain_segment(
1349        &mut self,
1350        ctx: &ExecutorContext,
1351        version: Version,
1352        sketch: ObjectId,
1353        previous_segment_end_point_id: ObjectId,
1354        segment: SegmentCtor,
1355        _label: Option<String>,
1356    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1357        // TODO: Check version.
1358
1359        // First, add the segment (line) to get its start point ID
1360        let SegmentCtor::Line(line_ctor) = segment else {
1361            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1362                "chain_segment currently only supports Line segments, got {}",
1363                segment.human_friendly_kind_with_article(),
1364            ))));
1365        };
1366
1367        // Add the line segment first - this updates self.program and self.scene_graph
1368        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1369
1370        // Find the new line's start point ID from the updated scene graph
1371        // add_line updates self.scene_graph, so we can use that
1372        let new_line_id = first_scene_delta
1373            .new_objects
1374            .iter()
1375            .find(|&obj_id| {
1376                let obj = self.scene_graph.objects.get(obj_id.0);
1377                if let Some(obj) = obj {
1378                    matches!(
1379                        &obj.kind,
1380                        ObjectKind::Segment {
1381                            segment: Segment::Line(_)
1382                        }
1383                    )
1384                } else {
1385                    false
1386                }
1387            })
1388            .ok_or_else(|| {
1389                KclErrorWithOutputs::no_outputs(KclError::refactor(
1390                    "Failed to find new line segment in scene graph".to_string(),
1391                ))
1392            })?;
1393
1394        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1395            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1396                "New line object not found: {new_line_id:?}"
1397            )))
1398        })?;
1399
1400        let ObjectKind::Segment {
1401            segment: new_line_segment,
1402        } = &new_line_obj.kind
1403        else {
1404            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1405                "Object is not a segment: {new_line_obj:?}"
1406            ))));
1407        };
1408
1409        let Segment::Line(new_line) = new_line_segment else {
1410            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1411                "Segment is not a line: {new_line_segment:?}"
1412            ))));
1413        };
1414
1415        let new_line_start_point_id = new_line.start;
1416
1417        // Now add the coincident constraint between the previous end point and the new line's start point.
1418        let coincident = Coincident {
1419            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1420        };
1421
1422        let (final_src_delta, final_scene_delta) = self
1423            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1424            .await?;
1425
1426        // Combine new objects from the line addition and the constraint addition.
1427        // Both add_line and add_constraint now populate new_objects correctly.
1428        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1429        combined_new_objects.extend(final_scene_delta.new_objects);
1430
1431        let scene_graph_delta = SceneGraphDelta {
1432            new_graph: self.scene_graph_for_ui(),
1433            invalidates_ids: false,
1434            new_objects: combined_new_objects,
1435            exec_outcome: final_scene_delta.exec_outcome,
1436        };
1437
1438        Ok((final_src_delta, scene_graph_delta))
1439    }
1440
1441    async fn edit_constraint(
1442        &mut self,
1443        ctx: &ExecutorContext,
1444        _version: Version,
1445        sketch: ObjectId,
1446        constraint_id: ObjectId,
1447        value_expression: String,
1448    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1449        // TODO: Check version.
1450        let sketch_block_ref =
1451            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1452
1453        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1454            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1455        })?;
1456        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1457            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1458                "Object is not a constraint: {constraint_id:?}"
1459            ))));
1460        }
1461
1462        let mut new_ast = self.program.ast.clone();
1463
1464        // Parse the expression string into an AST node.
1465        let (parsed, errors) = Program::parse(&value_expression)
1466            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1467        if !errors.is_empty() {
1468            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1469                "Error parsing value expression: {errors:?}"
1470            ))));
1471        }
1472        let mut parsed = parsed.ok_or_else(|| {
1473            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1474        })?;
1475        if parsed.ast.body.is_empty() {
1476            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1477                "Empty value expression".to_string(),
1478            )));
1479        }
1480        let first = parsed.ast.body.remove(0);
1481        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1482            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1483                "Value expression must be a simple expression".to_string(),
1484            )));
1485        };
1486
1487        let new_value: ast::BinaryPart = expr_stmt
1488            .inner
1489            .expression
1490            .try_into()
1491            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1492
1493        self.mutate_ast(
1494            &mut new_ast,
1495            constraint_id,
1496            AstMutateCommand::EditConstraintValue { value: new_value },
1497        )
1498        .map_err(KclErrorWithOutputs::no_outputs)?;
1499
1500        self.execute_after_edit(
1501            ctx,
1502            sketch,
1503            sketch_block_ref,
1504            &mut new_ast,
1505            ExecuteAfterEditOptions {
1506                segment_ids_edited: Default::default(),
1507                edit_kind: EditDeleteKind::Edit,
1508                commit_solved_initial_guesses: true,
1509            },
1510        )
1511        .await
1512    }
1513
1514    async fn edit_distance_constraint_label_position(
1515        &mut self,
1516        ctx: &ExecutorContext,
1517        _version: Version,
1518        sketch: ObjectId,
1519        constraint_id: ObjectId,
1520        label_position: Point2d<Number>,
1521        anchor_segment_ids: Vec<ObjectId>,
1522    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1523        // TODO: Check version.
1524        let sketch_block_ref =
1525            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1526
1527        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1528            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1529        })?;
1530        if !matches!(
1531            &object.kind,
1532            ObjectKind::Constraint {
1533                constraint: Constraint::Distance(_)
1534                    | Constraint::HorizontalDistance(_)
1535                    | Constraint::VerticalDistance(_)
1536                    | Constraint::Radius(_)
1537                    | Constraint::Diameter(_),
1538            }
1539        ) {
1540            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1541                "Object does not support labelPosition: {constraint_id:?}"
1542            ))));
1543        }
1544
1545        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1546            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1547                "Could not convert label position to AST: {err}"
1548            )))
1549        })?;
1550        let mut new_ast = self.program.ast.clone();
1551        self.mutate_ast(
1552            &mut new_ast,
1553            constraint_id,
1554            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1555        )
1556        .map_err(KclErrorWithOutputs::no_outputs)?;
1557        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1558
1559        self.execute_after_edit(
1560            ctx,
1561            sketch,
1562            sketch_block_ref,
1563            &mut new_ast,
1564            ExecuteAfterEditOptions {
1565                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1566                edit_kind: EditDeleteKind::Edit,
1567                commit_solved_initial_guesses,
1568            },
1569        )
1570        .await
1571    }
1572
1573    /// Splitting a segment means creating a new segment, editing the old one, and then
1574    /// migrating a bunch of the constraints from the original segment to the new one
1575    /// (i.e. deleting them and re-adding them on the other segment).
1576    ///
1577    /// To keep this efficient we require as few executions as possible: we create the
1578    /// new segment first (to get its id), then do all edits and new constraints, and
1579    /// do all deletes at the end (since deletes invalidate ids).
1580    async fn batch_split_segment_operations(
1581        &mut self,
1582        ctx: &ExecutorContext,
1583        _version: Version,
1584        sketch: ObjectId,
1585        edit_segments: Vec<ExistingSegmentCtor>,
1586        add_constraints: Vec<Constraint>,
1587        delete_constraint_ids: Vec<ObjectId>,
1588        _new_segment_info: sketch::NewSegmentInfo,
1589    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1590        // TODO: Check version.
1591        let sketch_block_ref =
1592            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1593
1594        let mut new_ast = self.program.ast.clone();
1595        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1596
1597        // Step 1: Edit segments
1598        for segment in edit_segments {
1599            segment_ids_edited.insert(segment.id);
1600            match segment.ctor {
1601                SegmentCtor::Point(ctor) => self
1602                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1603                    .map_err(KclErrorWithOutputs::no_outputs)?,
1604                SegmentCtor::Line(ctor) => self
1605                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1606                    .map_err(KclErrorWithOutputs::no_outputs)?,
1607                SegmentCtor::Arc(ctor) => self
1608                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1609                    .map_err(KclErrorWithOutputs::no_outputs)?,
1610                SegmentCtor::Circle(ctor) => self
1611                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1612                    .map_err(KclErrorWithOutputs::no_outputs)?,
1613                SegmentCtor::ControlPointSpline(ctor) => self
1614                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1615                    .map_err(KclErrorWithOutputs::no_outputs)?,
1616            }
1617        }
1618
1619        // Step 2: Add all constraints
1620        for constraint in add_constraints {
1621            match constraint {
1622                Constraint::Coincident(coincident) => {
1623                    self.add_coincident(sketch, coincident, &mut new_ast)
1624                        .await
1625                        .map_err(KclErrorWithOutputs::no_outputs)?;
1626                }
1627                Constraint::Distance(distance) => {
1628                    self.add_distance(sketch, distance, &mut new_ast)
1629                        .await
1630                        .map_err(KclErrorWithOutputs::no_outputs)?;
1631                }
1632                Constraint::EqualRadius(equal_radius) => {
1633                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1634                        .await
1635                        .map_err(KclErrorWithOutputs::no_outputs)?;
1636                }
1637                Constraint::Fixed(fixed) => {
1638                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1639                        .await
1640                        .map_err(KclErrorWithOutputs::no_outputs)?;
1641                }
1642                Constraint::HorizontalDistance(distance) => {
1643                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1644                        .await
1645                        .map_err(KclErrorWithOutputs::no_outputs)?;
1646                }
1647                Constraint::VerticalDistance(distance) => {
1648                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1649                        .await
1650                        .map_err(KclErrorWithOutputs::no_outputs)?;
1651                }
1652                Constraint::Horizontal(horizontal) => {
1653                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1654                        .await
1655                        .map_err(KclErrorWithOutputs::no_outputs)?;
1656                }
1657                Constraint::LinesEqualLength(lines_equal_length) => {
1658                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1659                        .await
1660                        .map_err(KclErrorWithOutputs::no_outputs)?;
1661                }
1662                Constraint::Midpoint(midpoint) => {
1663                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1664                        .await
1665                        .map_err(KclErrorWithOutputs::no_outputs)?;
1666                }
1667                Constraint::Parallel(parallel) => {
1668                    self.add_parallel(sketch, parallel, &mut new_ast)
1669                        .await
1670                        .map_err(KclErrorWithOutputs::no_outputs)?;
1671                }
1672                Constraint::Perpendicular(perpendicular) => {
1673                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1674                        .await
1675                        .map_err(KclErrorWithOutputs::no_outputs)?;
1676                }
1677                Constraint::Vertical(vertical) => {
1678                    self.add_vertical(sketch, vertical, &mut new_ast)
1679                        .await
1680                        .map_err(KclErrorWithOutputs::no_outputs)?;
1681                }
1682                Constraint::Diameter(diameter) => {
1683                    self.add_diameter(sketch, diameter, &mut new_ast)
1684                        .await
1685                        .map_err(KclErrorWithOutputs::no_outputs)?;
1686                }
1687                Constraint::Radius(radius) => {
1688                    self.add_radius(sketch, radius, &mut new_ast)
1689                        .await
1690                        .map_err(KclErrorWithOutputs::no_outputs)?;
1691                }
1692                Constraint::Symmetric(symmetric) => {
1693                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1694                        .await
1695                        .map_err(KclErrorWithOutputs::no_outputs)?;
1696                }
1697                Constraint::Angle(angle) => {
1698                    self.add_angle(sketch, angle, &mut new_ast)
1699                        .await
1700                        .map_err(KclErrorWithOutputs::no_outputs)?;
1701                }
1702                Constraint::Tangent(tangent) => {
1703                    self.add_tangent(sketch, tangent, &mut new_ast)
1704                        .await
1705                        .map_err(KclErrorWithOutputs::no_outputs)?;
1706                }
1707            }
1708        }
1709
1710        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1711        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1712
1713        let has_constraint_deletions = !constraint_ids_set.is_empty();
1714        for constraint_id in constraint_ids_set {
1715            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1716                .map_err(KclErrorWithOutputs::no_outputs)?;
1717        }
1718
1719        // Step 4: Execute once at the end
1720        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1721        // But we'll manually set invalidates_ids: true if we deleted constraints
1722        let (source_delta, mut scene_graph_delta) = self
1723            .execute_after_edit(
1724                ctx,
1725                sketch,
1726                sketch_block_ref,
1727                &mut new_ast,
1728                ExecuteAfterEditOptions {
1729                    segment_ids_edited,
1730                    edit_kind: EditDeleteKind::Edit,
1731                    commit_solved_initial_guesses: true,
1732                },
1733            )
1734            .await?;
1735
1736        // If we deleted constraints, set invalidates_ids: true
1737        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1738        if has_constraint_deletions {
1739            scene_graph_delta.invalidates_ids = true;
1740        }
1741
1742        Ok((source_delta, scene_graph_delta))
1743    }
1744
1745    async fn batch_tail_cut_operations(
1746        &mut self,
1747        ctx: &ExecutorContext,
1748        _version: Version,
1749        sketch: ObjectId,
1750        edit_segments: Vec<ExistingSegmentCtor>,
1751        add_constraints: Vec<Constraint>,
1752        delete_constraint_ids: Vec<ObjectId>,
1753        additional_edited_segment_ids: Vec<ObjectId>,
1754    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1755        let sketch_block_ref =
1756            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1757
1758        let mut new_ast = self.program.ast.clone();
1759        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1760
1761        // Step 1: Edit segments (usually a single segment for tail cut)
1762        for segment in edit_segments {
1763            segment_ids_edited.insert(segment.id);
1764            match segment.ctor {
1765                SegmentCtor::Point(ctor) => self
1766                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1767                    .map_err(KclErrorWithOutputs::no_outputs)?,
1768                SegmentCtor::Line(ctor) => self
1769                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1770                    .map_err(KclErrorWithOutputs::no_outputs)?,
1771                SegmentCtor::Arc(ctor) => self
1772                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1773                    .map_err(KclErrorWithOutputs::no_outputs)?,
1774                SegmentCtor::Circle(ctor) => self
1775                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1776                    .map_err(KclErrorWithOutputs::no_outputs)?,
1777                SegmentCtor::ControlPointSpline(ctor) => self
1778                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1779                    .map_err(KclErrorWithOutputs::no_outputs)?,
1780            }
1781        }
1782
1783        segment_ids_edited.extend(additional_edited_segment_ids);
1784
1785        // Step 2: Add coincident constraints
1786        for constraint in add_constraints {
1787            match constraint {
1788                Constraint::Coincident(coincident) => {
1789                    self.add_coincident(sketch, coincident, &mut new_ast)
1790                        .await
1791                        .map_err(KclErrorWithOutputs::no_outputs)?;
1792                }
1793                other => {
1794                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1795                        "unsupported constraint in tail cut batch: {other:?}"
1796                    ))));
1797                }
1798            }
1799        }
1800
1801        // Step 3: Delete constraints (if any)
1802        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1803
1804        let has_constraint_deletions = !constraint_ids_set.is_empty();
1805        for constraint_id in constraint_ids_set {
1806            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1807                .map_err(KclErrorWithOutputs::no_outputs)?;
1808        }
1809
1810        // Step 4: Single execute_after_edit
1811        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1812        // But we'll manually set invalidates_ids: true if we deleted constraints
1813        let (source_delta, mut scene_graph_delta) = self
1814            .execute_after_edit(
1815                ctx,
1816                sketch,
1817                sketch_block_ref,
1818                &mut new_ast,
1819                ExecuteAfterEditOptions {
1820                    segment_ids_edited,
1821                    edit_kind: EditDeleteKind::Edit,
1822                    commit_solved_initial_guesses: true,
1823                },
1824            )
1825            .await?;
1826
1827        // If we deleted constraints, set invalidates_ids: true
1828        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1829        if has_constraint_deletions {
1830            scene_graph_delta.invalidates_ids = true;
1831        }
1832
1833        Ok((source_delta, scene_graph_delta))
1834    }
1835}
1836
1837impl FrontendState {
1838    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1839        self.program = program.clone();
1840
1841        // Execute so that the objects are updated and available for the next
1842        // API call.
1843        // This always uses engine execution (not mock) so that things are cached.
1844        // Engine execution now runs freedom analysis automatically.
1845        // Keep existing checkpoints alive here. History may still reference
1846        // older committed sketch states across a direct-edit boundary, and a
1847        // checkpoint restore is a full state replacement anyway. We append a
1848        // fresh baseline checkpoint after the full execution below.
1849        // Clear the freedom cache since IDs might have changed after direct editing
1850        // and we're about to run freedom analysis which will repopulate it.
1851        self.point_freedom_cache.clear();
1852        match ctx.run_with_caching(program).await {
1853            Ok(outcome) => {
1854                let outcome = self.update_state_after_exec(outcome, true);
1855                let checkpoint_id = self
1856                    .create_sketch_checkpoint(outcome.clone())
1857                    .await
1858                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1859                Ok(SetProgramOutcome::Success {
1860                    scene_graph: Box::new(self.scene_graph_for_ui()),
1861                    exec_outcome: Box::new(outcome),
1862                    checkpoint_id: Some(checkpoint_id),
1863                })
1864            }
1865            Err(mut err) => {
1866                // Don't return an error just because execution failed. Instead,
1867                // update state as much as possible.
1868                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1869                self.update_state_after_exec(outcome, true);
1870                err.scene_graph = Some(self.scene_graph_for_ui());
1871                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1872            }
1873        }
1874    }
1875
1876    /// Decorate engine execution such that our state is updated and the scene
1877    /// graph is added to the return.
1878    pub async fn engine_execute(
1879        &mut self,
1880        ctx: &ExecutorContext,
1881        program: Program,
1882    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1883        self.program = program.clone();
1884
1885        // Engine execution now runs freedom analysis automatically. Clear the
1886        // freedom cache since IDs might have changed after direct editing, and
1887        // we're about to run freedom analysis which will repopulate it.
1888        self.point_freedom_cache.clear();
1889        match ctx.run_with_caching(program).await {
1890            Ok(outcome) => {
1891                let outcome = self.update_state_after_exec(outcome, true);
1892                Ok(SceneGraphDelta {
1893                    new_graph: self.scene_graph_for_ui(),
1894                    exec_outcome: outcome,
1895                    // We don't know what the new objects are.
1896                    new_objects: Default::default(),
1897                    // We don't know if IDs were invalidated.
1898                    invalidates_ids: Default::default(),
1899                })
1900            }
1901            Err(mut err) => {
1902                // Update state as much as possible, even when there's an error.
1903                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1904                self.update_state_after_exec(outcome, true);
1905                err.scene_graph = Some(self.scene_graph_for_ui());
1906                Err(err)
1907            }
1908        }
1909    }
1910
1911    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1912        if matches!(err.error, KclError::EngineHangup { .. }) {
1913            // It's not ideal to special-case this, but this error is very
1914            // common during development, and it causes confusing downstream
1915            // errors that have nothing to do with the actual problem.
1916            return Err(err);
1917        }
1918
1919        let KclErrorWithOutputs {
1920            error,
1921            mut non_fatal,
1922            variables,
1923            operations,
1924            artifact_graph,
1925            scene_objects,
1926            source_range_to_object,
1927            var_solutions,
1928            filenames,
1929            default_planes,
1930            ..
1931        } = err;
1932
1933        if let Some(source_range) = error.source_ranges().first() {
1934            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1935        } else {
1936            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1937        }
1938
1939        Ok(ExecOutcome {
1940            variables,
1941            filenames,
1942            operations,
1943            artifact_graph,
1944            scene_objects,
1945            source_range_to_object,
1946            var_solutions,
1947            issues: non_fatal,
1948            default_planes,
1949        })
1950    }
1951
1952    async fn add_point(
1953        &mut self,
1954        ctx: &ExecutorContext,
1955        sketch: ObjectId,
1956        ctor: PointCtor,
1957    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1958        // Create updated KCL source from args.
1959        let at_ast = to_ast_point2d(&ctor.position)
1960            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1961        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1962            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1963            unlabeled: None,
1964            arguments: vec![ast::LabeledArg {
1965                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1966                arg: at_ast,
1967            }],
1968            digest: None,
1969            non_code_meta: Default::default(),
1970        })));
1971
1972        // Look up existing sketch.
1973        let sketch_id = sketch;
1974        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1975            #[cfg(target_arch = "wasm32")]
1976            web_sys::console::error_1(
1977                &format!(
1978                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1979                    &self.scene_graph.objects
1980                )
1981                .into(),
1982            );
1983            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1984        })?;
1985        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1986            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1987                "Object is not a sketch, it is {}",
1988                sketch_object.kind.human_friendly_kind_with_article(),
1989            ))));
1990        };
1991        // Add the point to the AST of the sketch block.
1992        let mut new_ast = self.program.ast.clone();
1993        let (sketch_block_ref, _) = self
1994            .mutate_ast(
1995                &mut new_ast,
1996                sketch_id,
1997                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1998            )
1999            .map_err(KclErrorWithOutputs::no_outputs)?;
2000        // Convert to string source to create real source ranges.
2001        let new_source = source_from_ast(&new_ast);
2002        // Parse the new KCL source.
2003        let (new_program, errors) = Program::parse(&new_source)
2004            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2005        if !errors.is_empty() {
2006            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2007                "Error parsing KCL source after adding point: {errors:?}"
2008            ))));
2009        }
2010        let Some(new_program) = new_program else {
2011            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2012                "No AST produced after adding point".to_string(),
2013            )));
2014        };
2015
2016        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2017            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2018                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2019            )))
2020        })?;
2021
2022        // Make sure to only set this if there are no errors.
2023        self.program = new_program.clone();
2024
2025        // Truncate after the sketch block for mock execution.
2026        let mut truncated_program = new_program;
2027        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2028            .map_err(KclErrorWithOutputs::no_outputs)?;
2029
2030        // Execute.
2031        let outcome = ctx
2032            .run_mock(
2033                &truncated_program,
2034                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2035            )
2036            .await?;
2037
2038        let new_object_ids = {
2039            let make_err =
2040                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2041            let segment_id = outcome
2042                .source_range_to_object
2043                .get(&point_node_ref.range)
2044                .copied()
2045                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2046            let segment_object = outcome
2047                .scene_objects
2048                .get(segment_id.0)
2049                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2050            let ObjectKind::Segment { segment } = &segment_object.kind else {
2051                return Err(make_err(format!(
2052                    "Object is not a segment, it is {}",
2053                    segment_object.kind.human_friendly_kind_with_article()
2054                )));
2055            };
2056            let Segment::Point(_) = segment else {
2057                return Err(make_err(format!(
2058                    "Segment is not a point, it is {}",
2059                    segment.human_friendly_kind_with_article()
2060                )));
2061            };
2062            vec![segment_id]
2063        };
2064        let src_delta = SourceDelta { text: new_source };
2065        // Uses .no_freedom_analysis() so freedom_analysis: false
2066        let outcome = self.update_state_after_exec(outcome, false);
2067        let scene_graph_delta = SceneGraphDelta {
2068            new_graph: self.scene_graph_for_ui(),
2069            invalidates_ids: false,
2070            new_objects: new_object_ids,
2071            exec_outcome: outcome,
2072        };
2073        Ok((src_delta, scene_graph_delta))
2074    }
2075
2076    async fn add_line(
2077        &mut self,
2078        ctx: &ExecutorContext,
2079        sketch: ObjectId,
2080        ctor: LineCtor,
2081    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2082        // Create updated KCL source from args.
2083        let start_ast = to_ast_point2d(&ctor.start)
2084            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2085        let end_ast = to_ast_point2d(&ctor.end)
2086            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087        let mut arguments = vec![
2088            ast::LabeledArg {
2089                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2090                arg: start_ast,
2091            },
2092            ast::LabeledArg {
2093                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2094                arg: end_ast,
2095            },
2096        ];
2097        // Add construction kwarg if construction is Some(true)
2098        if ctor.construction == Some(true) {
2099            arguments.push(ast::LabeledArg {
2100                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2101                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2102                    value: ast::LiteralValue::Bool(true),
2103                    raw: "true".to_string(),
2104                    digest: None,
2105                }))),
2106            });
2107        }
2108        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2109            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2110            unlabeled: None,
2111            arguments,
2112            digest: None,
2113            non_code_meta: Default::default(),
2114        })));
2115
2116        // Look up existing sketch.
2117        let sketch_id = sketch;
2118        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2119            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2120        })?;
2121        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2122            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2123                "Object is not a sketch, it is {}",
2124                sketch_object.kind.human_friendly_kind_with_article(),
2125            ))));
2126        };
2127        // Add the line to the AST of the sketch block.
2128        let mut new_ast = self.program.ast.clone();
2129        let (sketch_block_ref, _) = self
2130            .mutate_ast(
2131                &mut new_ast,
2132                sketch_id,
2133                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2134            )
2135            .map_err(KclErrorWithOutputs::no_outputs)?;
2136        // Convert to string source to create real source ranges.
2137        let new_source = source_from_ast(&new_ast);
2138        // Parse the new KCL source.
2139        let (new_program, errors) = Program::parse(&new_source)
2140            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2141        if !errors.is_empty() {
2142            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2143                "Error parsing KCL source after adding line: {errors:?}"
2144            ))));
2145        }
2146        let Some(new_program) = new_program else {
2147            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2148                "No AST produced after adding line".to_string(),
2149            )));
2150        };
2151
2152        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2153            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2154                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2155            )))
2156        })?;
2157
2158        // Make sure to only set this if there are no errors.
2159        self.program = new_program.clone();
2160
2161        // Truncate after the sketch block for mock execution.
2162        let mut truncated_program = new_program;
2163        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2164            .map_err(KclErrorWithOutputs::no_outputs)?;
2165
2166        // Execute.
2167        let outcome = ctx
2168            .run_mock(
2169                &truncated_program,
2170                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2171            )
2172            .await?;
2173
2174        let new_object_ids = {
2175            let make_err =
2176                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2177            let segment_id = outcome
2178                .source_range_to_object
2179                .get(&line_node_ref.range)
2180                .copied()
2181                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2182            let segment_object = outcome
2183                .scene_object_by_id(segment_id)
2184                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2185            let ObjectKind::Segment { segment } = &segment_object.kind else {
2186                return Err(make_err(format!(
2187                    "Object is not a segment, it is {}",
2188                    segment_object.kind.human_friendly_kind_with_article()
2189                )));
2190            };
2191            let Segment::Line(line) = segment else {
2192                return Err(make_err(format!(
2193                    "Segment is not a line, it is {}",
2194                    segment.human_friendly_kind_with_article()
2195                )));
2196            };
2197            vec![line.start, line.end, segment_id]
2198        };
2199        let src_delta = SourceDelta { text: new_source };
2200        // Uses .no_freedom_analysis() so freedom_analysis: false
2201        let outcome = self.update_state_after_exec(outcome, false);
2202        let scene_graph_delta = SceneGraphDelta {
2203            new_graph: self.scene_graph_for_ui(),
2204            invalidates_ids: false,
2205            new_objects: new_object_ids,
2206            exec_outcome: outcome,
2207        };
2208        Ok((src_delta, scene_graph_delta))
2209    }
2210
2211    async fn add_arc(
2212        &mut self,
2213        ctx: &ExecutorContext,
2214        sketch: ObjectId,
2215        ctor: ArcCtor,
2216    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2217        // Create updated KCL source from args.
2218        let start_ast = to_ast_point2d(&ctor.start)
2219            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2220        let end_ast = to_ast_point2d(&ctor.end)
2221            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2222        let center_ast = to_ast_point2d(&ctor.center)
2223            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2224        let mut arguments = vec![
2225            ast::LabeledArg {
2226                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2227                arg: start_ast,
2228            },
2229            ast::LabeledArg {
2230                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2231                arg: end_ast,
2232            },
2233            ast::LabeledArg {
2234                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2235                arg: center_ast,
2236            },
2237        ];
2238        // Add construction kwarg if construction is Some(true)
2239        if ctor.construction == Some(true) {
2240            arguments.push(ast::LabeledArg {
2241                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2242                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2243                    value: ast::LiteralValue::Bool(true),
2244                    raw: "true".to_string(),
2245                    digest: None,
2246                }))),
2247            });
2248        }
2249        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2250            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2251            unlabeled: None,
2252            arguments,
2253            digest: None,
2254            non_code_meta: Default::default(),
2255        })));
2256
2257        // Look up existing sketch.
2258        let sketch_id = sketch;
2259        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2260            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2261        })?;
2262        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2263            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2264                "Object is not a sketch, it is {}",
2265                sketch_object.kind.human_friendly_kind_with_article(),
2266            ))));
2267        };
2268        // Add the arc to the AST of the sketch block.
2269        let mut new_ast = self.program.ast.clone();
2270        let (sketch_block_ref, _) = self
2271            .mutate_ast(
2272                &mut new_ast,
2273                sketch_id,
2274                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2275            )
2276            .map_err(KclErrorWithOutputs::no_outputs)?;
2277        // Convert to string source to create real source ranges.
2278        let new_source = source_from_ast(&new_ast);
2279        // Parse the new KCL source.
2280        let (new_program, errors) = Program::parse(&new_source)
2281            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2282        if !errors.is_empty() {
2283            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2284                "Error parsing KCL source after adding arc: {errors:?}"
2285            ))));
2286        }
2287        let Some(new_program) = new_program else {
2288            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2289                "No AST produced after adding arc".to_string(),
2290            )));
2291        };
2292
2293        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2294            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2295                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2296            )))
2297        })?;
2298
2299        // Make sure to only set this if there are no errors.
2300        self.program = new_program.clone();
2301
2302        // Truncate after the sketch block for mock execution.
2303        let mut truncated_program = new_program;
2304        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2305            .map_err(KclErrorWithOutputs::no_outputs)?;
2306
2307        // Execute.
2308        let outcome = ctx
2309            .run_mock(
2310                &truncated_program,
2311                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2312            )
2313            .await?;
2314
2315        let new_object_ids = {
2316            let make_err =
2317                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2318            let segment_id = outcome
2319                .source_range_to_object
2320                .get(&arc_node_ref.range)
2321                .copied()
2322                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2323            let segment_object = outcome
2324                .scene_objects
2325                .get(segment_id.0)
2326                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2327            let ObjectKind::Segment { segment } = &segment_object.kind else {
2328                return Err(make_err(format!(
2329                    "Object is not a segment, it is {}",
2330                    segment_object.kind.human_friendly_kind_with_article()
2331                )));
2332            };
2333            let Segment::Arc(arc) = segment else {
2334                return Err(make_err(format!(
2335                    "Segment is not an arc, it is {}",
2336                    segment.human_friendly_kind_with_article()
2337                )));
2338            };
2339            vec![arc.start, arc.end, arc.center, segment_id]
2340        };
2341        let src_delta = SourceDelta { text: new_source };
2342        // Uses .no_freedom_analysis() so freedom_analysis: false
2343        let outcome = self.update_state_after_exec(outcome, false);
2344        let scene_graph_delta = SceneGraphDelta {
2345            new_graph: self.scene_graph_for_ui(),
2346            invalidates_ids: false,
2347            new_objects: new_object_ids,
2348            exec_outcome: outcome,
2349        };
2350        Ok((src_delta, scene_graph_delta))
2351    }
2352
2353    async fn add_circle(
2354        &mut self,
2355        ctx: &ExecutorContext,
2356        sketch: ObjectId,
2357        ctor: CircleCtor,
2358    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2359        // Create updated KCL source from args.
2360        let start_ast = to_ast_point2d(&ctor.start)
2361            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2362        let center_ast = to_ast_point2d(&ctor.center)
2363            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2364        let mut arguments = vec![
2365            ast::LabeledArg {
2366                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2367                arg: start_ast,
2368            },
2369            ast::LabeledArg {
2370                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2371                arg: center_ast,
2372            },
2373        ];
2374        // Add construction kwarg if construction is Some(true)
2375        if ctor.construction == Some(true) {
2376            arguments.push(ast::LabeledArg {
2377                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2378                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2379                    value: ast::LiteralValue::Bool(true),
2380                    raw: "true".to_string(),
2381                    digest: None,
2382                }))),
2383            });
2384        }
2385        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2386            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2387            unlabeled: None,
2388            arguments,
2389            digest: None,
2390            non_code_meta: Default::default(),
2391        })));
2392
2393        // Look up existing sketch.
2394        let sketch_id = sketch;
2395        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2396            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2397        })?;
2398        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2399            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2400                "Object is not a sketch, it is {}",
2401                sketch_object.kind.human_friendly_kind_with_article(),
2402            ))));
2403        };
2404        // Add the circle to the AST of the sketch block.
2405        let mut new_ast = self.program.ast.clone();
2406        let (sketch_block_ref, _) = self
2407            .mutate_ast(
2408                &mut new_ast,
2409                sketch_id,
2410                AstMutateCommand::AddSketchBlockVarDecl {
2411                    prefix: CIRCLE_VARIABLE.to_owned(),
2412                    expr: circle_ast,
2413                },
2414            )
2415            .map_err(KclErrorWithOutputs::no_outputs)?;
2416        // Convert to string source to create real source ranges.
2417        let new_source = source_from_ast(&new_ast);
2418        // Parse the new KCL source.
2419        let (new_program, errors) = Program::parse(&new_source)
2420            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2421        if !errors.is_empty() {
2422            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2423                "Error parsing KCL source after adding circle: {errors:?}"
2424            ))));
2425        }
2426        let Some(new_program) = new_program else {
2427            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2428                "No AST produced after adding circle".to_string(),
2429            )));
2430        };
2431
2432        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2433            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2434                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2435            )))
2436        })?;
2437
2438        // Make sure to only set this if there are no errors.
2439        self.program = new_program.clone();
2440
2441        // Truncate after the sketch block for mock execution.
2442        let mut truncated_program = new_program;
2443        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2444            .map_err(KclErrorWithOutputs::no_outputs)?;
2445
2446        // Execute.
2447        let outcome = ctx
2448            .run_mock(
2449                &truncated_program,
2450                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2451            )
2452            .await?;
2453
2454        let new_object_ids = {
2455            let make_err =
2456                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2457            let segment_id = outcome
2458                .source_range_to_object
2459                .get(&circle_node_ref.range)
2460                .copied()
2461                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2462            let segment_object = outcome
2463                .scene_objects
2464                .get(segment_id.0)
2465                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2466            let ObjectKind::Segment { segment } = &segment_object.kind else {
2467                return Err(make_err(format!(
2468                    "Object is not a segment, it is {}",
2469                    segment_object.kind.human_friendly_kind_with_article()
2470                )));
2471            };
2472            let Segment::Circle(circle) = segment else {
2473                return Err(make_err(format!(
2474                    "Segment is not a circle, it is {}",
2475                    segment.human_friendly_kind_with_article()
2476                )));
2477            };
2478            vec![circle.start, circle.center, segment_id]
2479        };
2480        let src_delta = SourceDelta { text: new_source };
2481        // Uses .no_freedom_analysis() so freedom_analysis: false
2482        let outcome = self.update_state_after_exec(outcome, false);
2483        let scene_graph_delta = SceneGraphDelta {
2484            new_graph: self.scene_graph_for_ui(),
2485            invalidates_ids: false,
2486            new_objects: new_object_ids,
2487            exec_outcome: outcome,
2488        };
2489        Ok((src_delta, scene_graph_delta))
2490    }
2491
2492    async fn add_control_point_spline(
2493        &mut self,
2494        ctx: &ExecutorContext,
2495        sketch: ObjectId,
2496        ctor: ControlPointSplineCtor,
2497    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2498        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2499            .map_err(KclErrorWithOutputs::no_outputs)?;
2500
2501        let points_ast = to_ast_point2d_array(&ctor.points)
2502            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2503        let mut arguments = vec![ast::LabeledArg {
2504            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2505            arg: points_ast,
2506        }];
2507        if ctor.construction == Some(true) {
2508            arguments.push(ast::LabeledArg {
2509                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2510                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2511                    value: ast::LiteralValue::Bool(true),
2512                    raw: "true".to_string(),
2513                    digest: None,
2514                }))),
2515            });
2516        }
2517        let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2518            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2519            unlabeled: None,
2520            arguments,
2521            digest: None,
2522            non_code_meta: Default::default(),
2523        })));
2524
2525        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2526            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2527        })?;
2528        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2529            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2530                "Object is not a sketch, it is {}",
2531                sketch_object.kind.human_friendly_kind_with_article(),
2532            ))));
2533        };
2534
2535        let mut new_ast = new_program.ast.clone();
2536        let (sketch_block_ref, _) = self
2537            .mutate_ast(
2538                &mut new_ast,
2539                sketch,
2540                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2541            )
2542            .map_err(KclErrorWithOutputs::no_outputs)?;
2543        let new_source = source_from_ast(&new_ast);
2544        let (new_program, errors) = Program::parse(&new_source)
2545            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2546        if !errors.is_empty() {
2547            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2548                "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2549            ))));
2550        }
2551        let Some(new_program) = new_program else {
2552            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2553                "No AST produced after adding controlPointSpline".to_string(),
2554            )));
2555        };
2556
2557        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2558            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2559                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2560            )))
2561        })?;
2562
2563        self.program = new_program.clone();
2564
2565        let mut truncated_program = new_program;
2566        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2567            .map_err(KclErrorWithOutputs::no_outputs)?;
2568
2569        let outcome = ctx
2570            .run_mock(
2571                &truncated_program,
2572                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2573            )
2574            .await?;
2575
2576        let new_object_ids = {
2577            let make_err =
2578                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2579            let segment_id = outcome
2580                .source_range_to_object
2581                .get(&spline_node_ref.range)
2582                .copied()
2583                .ok_or_else(|| {
2584                    make_err(format!(
2585                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2586                    ))
2587                })?;
2588            let segment_object = outcome
2589                .scene_objects
2590                .get(segment_id.0)
2591                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2592            let ObjectKind::Segment { segment } = &segment_object.kind else {
2593                return Err(make_err(format!(
2594                    "Object is not a segment, it is {}",
2595                    segment_object.kind.human_friendly_kind_with_article()
2596                )));
2597            };
2598            let Segment::ControlPointSpline(spline) = segment else {
2599                return Err(make_err(format!(
2600                    "Segment is not a control point spline, it is {}",
2601                    segment.human_friendly_kind_with_article()
2602                )));
2603            };
2604
2605            let mut ids = outcome
2606                .scene_objects
2607                .iter()
2608                .filter_map(|obj| match &obj.kind {
2609                    ObjectKind::Segment {
2610                        segment: Segment::Line(line),
2611                    } if line.owner == Some(segment_id) => Some(obj.id),
2612                    _ => None,
2613                })
2614                .collect::<Vec<_>>();
2615            ids.extend(spline.controls.clone());
2616            ids.push(segment_id);
2617            ids
2618        };
2619        let src_delta = SourceDelta { text: new_source };
2620        let outcome = self.update_state_after_exec(outcome, false);
2621        let scene_graph_delta = SceneGraphDelta {
2622            new_graph: self.scene_graph_for_ui(),
2623            invalidates_ids: false,
2624            new_objects: new_object_ids,
2625            exec_outcome: outcome,
2626        };
2627        Ok((src_delta, scene_graph_delta))
2628    }
2629
2630    fn edit_point(
2631        &mut self,
2632        new_ast: &mut ast::Node<ast::Program>,
2633        sketch: ObjectId,
2634        point: ObjectId,
2635        ctor: PointCtor,
2636    ) -> Result<(), KclError> {
2637        // Create updated KCL source from args.
2638        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2639
2640        // Look up existing sketch.
2641        let sketch_id = sketch;
2642        let sketch_object = self
2643            .scene_graph
2644            .objects
2645            .get(sketch_id.0)
2646            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2647        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2648            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2649        };
2650        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2651            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2652        })?;
2653        // Look up existing point.
2654        let point_id = point;
2655        let point_object = self
2656            .scene_graph
2657            .objects
2658            .get(point_id.0)
2659            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2660        let ObjectKind::Segment {
2661            segment: Segment::Point(point),
2662        } = &point_object.kind
2663        else {
2664            return Err(KclError::refactor(format!(
2665                "Object is not a point segment: {point_object:?}"
2666            )));
2667        };
2668
2669        // If the point is part of a line or arc, edit the line/arc instead.
2670        if let Some(owner_id) = point.owner {
2671            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2672                KclError::refactor(format!(
2673                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2674                ))
2675            })?;
2676            let ObjectKind::Segment { segment } = &owner_object.kind else {
2677                return Err(KclError::refactor(format!(
2678                    "Internal: Owner of point is not a segment, but found {}",
2679                    owner_object.kind.human_friendly_kind_with_article()
2680                )));
2681            };
2682
2683            // Handle Line owner
2684            if let Segment::Line(line) = segment {
2685                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2686                    return Err(KclError::refactor(format!(
2687                        "Internal: Owner of point does not have line ctor, but found {}",
2688                        line.ctor.human_friendly_kind_with_article()
2689                    )));
2690                };
2691                let mut line_ctor = line_ctor.clone();
2692                // Which end of the line is this point?
2693                if line.start == point_id {
2694                    line_ctor.start = ctor.position;
2695                } else if line.end == point_id {
2696                    line_ctor.end = ctor.position;
2697                } else {
2698                    return Err(KclError::refactor(format!(
2699                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2700                    )));
2701                }
2702                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2703            }
2704
2705            // Handle Arc owner
2706            if let Segment::Arc(arc) = segment {
2707                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2708                    return Err(KclError::refactor(format!(
2709                        "Internal: Owner of point does not have arc ctor, but found {}",
2710                        arc.ctor.human_friendly_kind_with_article()
2711                    )));
2712                };
2713                let mut arc_ctor = arc_ctor.clone();
2714                // Which point of the arc is this? (center, start, or end)
2715                if arc.center == point_id {
2716                    arc_ctor.center = ctor.position;
2717                } else if arc.start == point_id {
2718                    arc_ctor.start = ctor.position;
2719                } else if arc.end == point_id {
2720                    arc_ctor.end = ctor.position;
2721                } else {
2722                    return Err(KclError::refactor(format!(
2723                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2724                    )));
2725                }
2726                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2727            }
2728
2729            // Handle Circle owner
2730            if let Segment::Circle(circle) = segment {
2731                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2732                    return Err(KclError::refactor(format!(
2733                        "Internal: Owner of point does not have circle ctor, but found {}",
2734                        circle.ctor.human_friendly_kind_with_article()
2735                    )));
2736                };
2737                let mut circle_ctor = circle_ctor.clone();
2738                if circle.center == point_id {
2739                    circle_ctor.center = ctor.position;
2740                } else if circle.start == point_id {
2741                    circle_ctor.start = ctor.position;
2742                } else {
2743                    return Err(KclError::refactor(format!(
2744                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2745                    )));
2746                }
2747                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2748            }
2749
2750            if let Segment::ControlPointSpline(spline) = segment {
2751                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2752                    return Err(KclError::refactor(format!(
2753                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2754                        spline.ctor.human_friendly_kind_with_article()
2755                    )));
2756                };
2757                let mut spline_ctor = spline_ctor.clone();
2758                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2759                    return Err(KclError::refactor(format!(
2760                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2761                    )));
2762                };
2763                spline_ctor.points[control_index] = ctor.position;
2764                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2765            }
2766
2767            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2768            // (fall through to the point editing logic below)
2769        }
2770
2771        // Modify the point AST.
2772        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2773        Ok(())
2774    }
2775
2776    fn edit_line(
2777        &mut self,
2778        new_ast: &mut ast::Node<ast::Program>,
2779        sketch: ObjectId,
2780        line: ObjectId,
2781        ctor: LineCtor,
2782    ) -> Result<(), KclError> {
2783        // Create updated KCL source from args.
2784        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2785        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2786
2787        // Look up existing sketch.
2788        let sketch_id = sketch;
2789        let sketch_object = self
2790            .scene_graph
2791            .objects
2792            .get(sketch_id.0)
2793            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2794        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2795            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2796        };
2797        sketch
2798            .segments
2799            .iter()
2800            .find(|o| **o == line)
2801            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2802        // Look up existing line.
2803        let line_id = line;
2804        let line_object = self
2805            .scene_graph
2806            .objects
2807            .get(line_id.0)
2808            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2809        let ObjectKind::Segment { .. } = &line_object.kind else {
2810            let kind = line_object.kind.human_friendly_kind_with_article();
2811            return Err(KclError::refactor(format!(
2812                "This constraint only works on Segments, but you selected {kind}"
2813            )));
2814        };
2815
2816        // Modify the line AST.
2817        self.mutate_ast(
2818            new_ast,
2819            line_id,
2820            AstMutateCommand::EditLine {
2821                start: new_start_ast,
2822                end: new_end_ast,
2823                construction: ctor.construction,
2824            },
2825        )?;
2826        Ok(())
2827    }
2828
2829    fn edit_arc(
2830        &mut self,
2831        new_ast: &mut ast::Node<ast::Program>,
2832        sketch: ObjectId,
2833        arc: ObjectId,
2834        ctor: ArcCtor,
2835    ) -> Result<(), KclError> {
2836        // Create updated KCL source from args.
2837        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2838        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2839        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2840
2841        // Look up existing sketch.
2842        let sketch_id = sketch;
2843        let sketch_object = self
2844            .scene_graph
2845            .objects
2846            .get(sketch_id.0)
2847            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2848        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2849            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2850        };
2851        sketch
2852            .segments
2853            .iter()
2854            .find(|o| **o == arc)
2855            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2856        // Look up existing arc.
2857        let arc_id = arc;
2858        let arc_object = self
2859            .scene_graph
2860            .objects
2861            .get(arc_id.0)
2862            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2863        let ObjectKind::Segment { .. } = &arc_object.kind else {
2864            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2865        };
2866
2867        // Modify the arc AST.
2868        self.mutate_ast(
2869            new_ast,
2870            arc_id,
2871            AstMutateCommand::EditArc {
2872                start: new_start_ast,
2873                end: new_end_ast,
2874                center: new_center_ast,
2875                construction: ctor.construction,
2876            },
2877        )?;
2878        Ok(())
2879    }
2880
2881    fn edit_circle(
2882        &mut self,
2883        new_ast: &mut ast::Node<ast::Program>,
2884        sketch: ObjectId,
2885        circle: ObjectId,
2886        ctor: CircleCtor,
2887    ) -> Result<(), KclError> {
2888        // Create updated KCL source from args.
2889        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2890        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2891
2892        // Look up existing sketch.
2893        let sketch_id = sketch;
2894        let sketch_object = self
2895            .scene_graph
2896            .objects
2897            .get(sketch_id.0)
2898            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2899        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2900            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2901        };
2902        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2903            KclError::refactor(format!(
2904                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2905            ))
2906        })?;
2907        // Look up existing circle.
2908        let circle_id = circle;
2909        let circle_object = self
2910            .scene_graph
2911            .objects
2912            .get(circle_id.0)
2913            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2914        let ObjectKind::Segment { .. } = &circle_object.kind else {
2915            return Err(KclError::refactor(format!(
2916                "Object is not a segment: {circle_object:?}"
2917            )));
2918        };
2919
2920        // Modify the circle AST.
2921        self.mutate_ast(
2922            new_ast,
2923            circle_id,
2924            AstMutateCommand::EditCircle {
2925                start: new_start_ast,
2926                center: new_center_ast,
2927                construction: ctor.construction,
2928            },
2929        )?;
2930        Ok(())
2931    }
2932
2933    fn edit_control_point_spline(
2934        &mut self,
2935        new_ast: &mut ast::Node<ast::Program>,
2936        sketch: ObjectId,
2937        spline: ObjectId,
2938        ctor: ControlPointSplineCtor,
2939    ) -> Result<(), KclError> {
2940        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942        let sketch_object = self
2943            .scene_graph
2944            .objects
2945            .get(sketch.0)
2946            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949        };
2950        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2951            KclError::refactor(format!(
2952                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2953            ))
2954        })?;
2955
2956        let spline_object =
2957            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2958                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2959            })?;
2960        let ObjectKind::Segment { .. } = &spline_object.kind else {
2961            return Err(KclError::refactor(format!(
2962                "Object is not a segment: {spline_object:?}"
2963            )));
2964        };
2965
2966        self.mutate_ast(
2967            new_ast,
2968            spline,
2969            AstMutateCommand::EditControlPointSpline {
2970                points: points_ast,
2971                construction: ctor.construction,
2972            },
2973        )?;
2974        Ok(())
2975    }
2976
2977    fn delete_segment(
2978        &mut self,
2979        new_ast: &mut ast::Node<ast::Program>,
2980        sketch: ObjectId,
2981        segment_id: ObjectId,
2982    ) -> Result<(), KclError> {
2983        // Look up existing sketch.
2984        let sketch_id = sketch;
2985        let sketch_object = self
2986            .scene_graph
2987            .objects
2988            .get(sketch_id.0)
2989            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2990        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2991            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2992        };
2993        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2994            KclError::refactor(format!(
2995                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2996            ))
2997        })?;
2998        // Look up existing segment.
2999        let segment_object =
3000            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3001                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3002            })?;
3003        let ObjectKind::Segment { .. } = &segment_object.kind else {
3004            return Err(KclError::refactor(format!(
3005                "Object is not a segment, it is {}",
3006                segment_object.kind.human_friendly_kind_with_article()
3007            )));
3008        };
3009
3010        // Modify the AST to remove the segment.
3011        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3012        Ok(())
3013    }
3014
3015    fn delete_constraint(
3016        &mut self,
3017        new_ast: &mut ast::Node<ast::Program>,
3018        sketch: ObjectId,
3019        constraint_id: ObjectId,
3020    ) -> Result<(), KclError> {
3021        // Look up existing sketch.
3022        let sketch_id = sketch;
3023        let sketch_object = self
3024            .scene_graph
3025            .objects
3026            .get(sketch_id.0)
3027            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3028        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3029            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3030        };
3031        sketch
3032            .constraints
3033            .iter()
3034            .find(|o| **o == constraint_id)
3035            .ok_or_else(|| {
3036                KclError::refactor(format!(
3037                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3038                ))
3039            })?;
3040        // Look up existing constraint.
3041        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3042            KclError::refactor(format!(
3043                "Constraint not found in scene graph: constraint={constraint_id:?}"
3044            ))
3045        })?;
3046        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3047            return Err(KclError::refactor(format!(
3048                "Object is not a constraint, it is {}",
3049                constraint_object.kind.human_friendly_kind_with_article()
3050            )));
3051        };
3052
3053        // Modify the AST to remove the constraint.
3054        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3055        Ok(())
3056    }
3057
3058    fn edit_coincident_constraint(
3059        &mut self,
3060        new_ast: &mut ast::Node<ast::Program>,
3061        constraint_id: ObjectId,
3062        segments: Vec<ConstraintSegment>,
3063    ) -> Result<(), KclError> {
3064        if segments.len() < 2 {
3065            return Err(KclError::refactor(format!(
3066                "Coincident constraint must have at least 2 inputs, got {}",
3067                segments.len()
3068            )));
3069        }
3070
3071        let segment_asts = segments
3072            .iter()
3073            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3074            .collect::<Result<Vec<_>, _>>()?;
3075
3076        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3077            elements: segment_asts,
3078            digest: None,
3079            non_code_meta: Default::default(),
3080        })));
3081
3082        self.mutate_ast(
3083            new_ast,
3084            constraint_id,
3085            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3086        )?;
3087        Ok(())
3088    }
3089
3090    fn edit_horizontal_points_constraint(
3091        &mut self,
3092        new_ast: &mut ast::Node<ast::Program>,
3093        constraint_id: ObjectId,
3094        points: Vec<ConstraintSegment>,
3095    ) -> Result<(), KclError> {
3096        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3097    }
3098
3099    fn edit_vertical_points_constraint(
3100        &mut self,
3101        new_ast: &mut ast::Node<ast::Program>,
3102        constraint_id: ObjectId,
3103        points: Vec<ConstraintSegment>,
3104    ) -> Result<(), KclError> {
3105        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3106    }
3107
3108    fn edit_axis_points_constraint(
3109        &mut self,
3110        new_ast: &mut ast::Node<ast::Program>,
3111        constraint_id: ObjectId,
3112        points: Vec<ConstraintSegment>,
3113        constraint_name: &str,
3114    ) -> Result<(), KclError> {
3115        if points.len() < 2 {
3116            return Err(KclError::refactor(format!(
3117                "{constraint_name} points constraint must have at least 2 points, got {}",
3118                points.len()
3119            )));
3120        }
3121
3122        let point_asts = points
3123            .iter()
3124            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3125            .collect::<Result<Vec<_>, _>>()?;
3126
3127        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3128            elements: point_asts,
3129            digest: None,
3130            non_code_meta: Default::default(),
3131        })));
3132
3133        self.mutate_ast(
3134            new_ast,
3135            constraint_id,
3136            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3137        )?;
3138        Ok(())
3139    }
3140
3141    /// updates the equalLength constraint with the given lines
3142    fn edit_equal_length_constraint(
3143        &mut self,
3144        new_ast: &mut ast::Node<ast::Program>,
3145        constraint_id: ObjectId,
3146        lines: Vec<ObjectId>,
3147    ) -> Result<(), KclError> {
3148        if lines.len() < 2 {
3149            return Err(KclError::refactor(format!(
3150                "Lines equal length constraint must have at least 2 lines, got {}",
3151                lines.len()
3152            )));
3153        }
3154
3155        let line_asts = lines
3156            .iter()
3157            .map(|line_id| {
3158                let line_object = self
3159                    .scene_graph
3160                    .objects
3161                    .get(line_id.0)
3162                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3163                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3164                    let kind = line_object.kind.human_friendly_kind_with_article();
3165                    return Err(KclError::refactor(format!(
3166                        "This constraint only works on Segments, but you selected {kind}"
3167                    )));
3168                };
3169                let Segment::Line(_) = line_segment else {
3170                    let kind = line_segment.human_friendly_kind_with_article();
3171                    return Err(KclError::refactor(format!(
3172                        "Only lines can be made equal length, but you selected {kind}"
3173                    )));
3174                };
3175
3176                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3177            })
3178            .collect::<Result<Vec<_>, _>>()?;
3179
3180        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3181            elements: line_asts,
3182            digest: None,
3183            non_code_meta: Default::default(),
3184        })));
3185
3186        self.mutate_ast(
3187            new_ast,
3188            constraint_id,
3189            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3190        )?;
3191        Ok(())
3192    }
3193
3194    /// Updates the parallel constraint with the given lines.
3195    fn edit_parallel_constraint(
3196        &mut self,
3197        new_ast: &mut ast::Node<ast::Program>,
3198        constraint_id: ObjectId,
3199        lines: Vec<ObjectId>,
3200    ) -> Result<(), KclError> {
3201        if lines.len() < 2 {
3202            return Err(KclError::refactor(format!(
3203                "Parallel constraint must have at least 2 lines, got {}",
3204                lines.len()
3205            )));
3206        }
3207
3208        let line_asts = lines
3209            .iter()
3210            .map(|line_id| {
3211                let line_object = self
3212                    .scene_graph
3213                    .objects
3214                    .get(line_id.0)
3215                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3216                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3217                    let kind = line_object.kind.human_friendly_kind_with_article();
3218                    return Err(KclError::refactor(format!(
3219                        "This constraint only works on Segments, but you selected {kind}"
3220                    )));
3221                };
3222                let Segment::Line(_) = line_segment else {
3223                    let kind = line_segment.human_friendly_kind_with_article();
3224                    return Err(KclError::refactor(format!(
3225                        "Only lines can be made parallel, but you selected {kind}"
3226                    )));
3227                };
3228
3229                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3230            })
3231            .collect::<Result<Vec<_>, _>>()?;
3232
3233        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3234            elements: line_asts,
3235            digest: None,
3236            non_code_meta: Default::default(),
3237        })));
3238
3239        self.mutate_ast(
3240            new_ast,
3241            constraint_id,
3242            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3243        )?;
3244        Ok(())
3245    }
3246
3247    /// Updates the equalRadius constraint with the given segments.
3248    fn edit_equal_radius_constraint(
3249        &mut self,
3250        new_ast: &mut ast::Node<ast::Program>,
3251        constraint_id: ObjectId,
3252        input: Vec<ObjectId>,
3253    ) -> Result<(), KclError> {
3254        if input.len() < 2 {
3255            return Err(KclError::refactor(format!(
3256                "equalRadius constraint must have at least 2 segments, got {}",
3257                input.len()
3258            )));
3259        }
3260
3261        let input_asts = input
3262            .iter()
3263            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3264            .collect::<Result<Vec<_>, _>>()?;
3265
3266        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3267            elements: input_asts,
3268            digest: None,
3269            non_code_meta: Default::default(),
3270        })));
3271
3272        self.mutate_ast(
3273            new_ast,
3274            constraint_id,
3275            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3276        )?;
3277        Ok(())
3278    }
3279
3280    async fn execute_after_edit(
3281        &mut self,
3282        ctx: &ExecutorContext,
3283        sketch: ObjectId,
3284        sketch_block_ref: AstNodeRef,
3285        new_ast: &mut ast::Node<ast::Program>,
3286        options: ExecuteAfterEditOptions,
3287    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3288        let ExecuteAfterEditOptions {
3289            segment_ids_edited,
3290            edit_kind,
3291            commit_solved_initial_guesses,
3292        } = options;
3293
3294        // Convert to string source to create real source ranges.
3295        let new_source = source_from_ast(new_ast);
3296        // Parse the new KCL source.
3297        let (new_program, errors) = Program::parse(&new_source)
3298            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3299        if !errors.is_empty() {
3300            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3301                "Error parsing KCL source after editing: {errors:?}"
3302            ))));
3303        }
3304        let Some(new_program) = new_program else {
3305            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3306                "No AST produced after editing".to_string(),
3307            )));
3308        };
3309
3310        // TODO: sketch-api: make sure to only set this if there are no errors.
3311        self.program = new_program.clone();
3312
3313        // Truncate after the sketch block for mock execution.
3314        let is_delete = edit_kind.is_delete();
3315        let truncated_program = {
3316            let mut truncated_program = new_program;
3317            only_sketch_block(
3318                &mut truncated_program.ast,
3319                &sketch_block_ref,
3320                edit_kind.to_change_kind(),
3321            )
3322            .map_err(KclErrorWithOutputs::no_outputs)?;
3323            truncated_program
3324        };
3325
3326        // Execute.
3327        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3328        let mock_config = MockConfig {
3329            sketch_block_id: Some(sketch),
3330            freedom_analysis: is_delete,
3331            segment_ids_edited: segment_ids_edited.clone(),
3332            drag_anchors,
3333            ..Default::default()
3334        };
3335        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3336
3337        // Uses freedom_analysis: is_delete
3338        let outcome = self.update_state_after_exec(outcome, is_delete);
3339
3340        let src_delta = if commit_solved_initial_guesses {
3341            self.commit_var_solutions_to_program(&outcome, "editing")?
3342        } else {
3343            SourceDelta { text: new_source }
3344        };
3345        let scene_graph_delta = SceneGraphDelta {
3346            new_graph: self.scene_graph_for_ui(),
3347            invalidates_ids: is_delete,
3348            new_objects: Vec::new(),
3349            exec_outcome: outcome,
3350        };
3351        Ok((src_delta, scene_graph_delta))
3352    }
3353
3354    async fn execute_after_delete_sketch(
3355        &mut self,
3356        ctx: &ExecutorContext,
3357        new_ast: &mut ast::Node<ast::Program>,
3358    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3359        // Convert to string source to create real source ranges.
3360        let new_source = source_from_ast(new_ast);
3361        // Parse the new KCL source.
3362        let (new_program, errors) = Program::parse(&new_source)
3363            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3364        if !errors.is_empty() {
3365            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3366                "Error parsing KCL source after editing: {errors:?}"
3367            ))));
3368        }
3369        let Some(new_program) = new_program else {
3370            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3371                "No AST produced after editing".to_string(),
3372            )));
3373        };
3374
3375        // Make sure to only set this if there are no errors.
3376        self.program = new_program.clone();
3377
3378        // We deleted the entire sketch block. It doesn't make sense to truncate
3379        // and execute only the sketch block. We execute the whole program with
3380        // a real engine.
3381
3382        // Execute.
3383        let outcome = ctx.run_with_caching(new_program).await?;
3384        let freedom_analysis_ran = true;
3385
3386        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3387
3388        let src_delta = SourceDelta { text: new_source };
3389        let scene_graph_delta = SceneGraphDelta {
3390            new_graph: self.scene_graph_for_ui(),
3391            invalidates_ids: true,
3392            new_objects: Vec::new(),
3393            exec_outcome: outcome,
3394        };
3395        Ok((src_delta, scene_graph_delta))
3396    }
3397
3398    /// Map a point object id into an AST reference expression for use in
3399    /// constraints. If the point is owned by a segment (line or arc), we
3400    /// reference the appropriate property on that segment (e.g. `line1.start`,
3401    /// `arc1.center`). Otherwise we reference the point directly.
3402    fn point_id_to_ast_reference(
3403        &self,
3404        point_id: ObjectId,
3405        new_ast: &mut ast::Node<ast::Program>,
3406    ) -> Result<ast::Expr, KclError> {
3407        let point_object = self
3408            .scene_graph
3409            .objects
3410            .get(point_id.0)
3411            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3412        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3413            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3414        };
3415        let Segment::Point(point) = point_segment else {
3416            return Err(KclError::refactor(format!(
3417                "Only points are currently supported: {point_object:?}"
3418            )));
3419        };
3420
3421        if let Some(owner_id) = point.owner {
3422            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3423                KclError::refactor(format!(
3424                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3425                ))
3426            })?;
3427            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3428                return Err(KclError::refactor(format!(
3429                    "Owner of point is not a segment, but found {}",
3430                    owner_object.kind.human_friendly_kind_with_article()
3431                )));
3432            };
3433
3434            match owner_segment {
3435                Segment::Line(line) => {
3436                    let property = if line.start == point_id {
3437                        LINE_PROPERTY_START
3438                    } else if line.end == point_id {
3439                        LINE_PROPERTY_END
3440                    } else {
3441                        return Err(KclError::refactor(format!(
3442                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3443                        )));
3444                    };
3445                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3446                }
3447                Segment::Arc(arc) => {
3448                    let property = if arc.start == point_id {
3449                        ARC_PROPERTY_START
3450                    } else if arc.end == point_id {
3451                        ARC_PROPERTY_END
3452                    } else if arc.center == point_id {
3453                        ARC_PROPERTY_CENTER
3454                    } else {
3455                        return Err(KclError::refactor(format!(
3456                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3457                        )));
3458                    };
3459                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3460                }
3461                Segment::Circle(circle) => {
3462                    let property = if circle.start == point_id {
3463                        CIRCLE_PROPERTY_START
3464                    } else if circle.center == point_id {
3465                        CIRCLE_PROPERTY_CENTER
3466                    } else {
3467                        return Err(KclError::refactor(format!(
3468                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3469                        )));
3470                    };
3471                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3472                }
3473                Segment::ControlPointSpline(spline) => {
3474                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3475                        return Err(KclError::refactor(format!(
3476                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3477                        )));
3478                    };
3479                    let owner_expr =
3480                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3481                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3482                    Ok(create_index_expression(controls_expr, index))
3483                }
3484                _ => Err(KclError::refactor(format!(
3485                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3486                ))),
3487            }
3488        } else {
3489            // Standalone point.
3490            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3491        }
3492    }
3493
3494    fn line_id_to_ast_reference(
3495        &self,
3496        line_id: ObjectId,
3497        new_ast: &mut ast::Node<ast::Program>,
3498    ) -> Result<ast::Expr, KclError> {
3499        let line_object = self
3500            .scene_graph
3501            .objects
3502            .get(line_id.0)
3503            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3504        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3505            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3506        };
3507        let Segment::Line(line) = line_segment else {
3508            return Err(KclError::refactor(format!(
3509                "Only lines are currently supported: {line_object:?}"
3510            )));
3511        };
3512
3513        if let Some(owner_id) = line.owner {
3514            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3515                KclError::refactor(format!(
3516                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3517                ))
3518            })?;
3519            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3520                return Err(KclError::refactor(format!(
3521                    "Owner of line is not a segment, but found {}",
3522                    owner_object.kind.human_friendly_kind_with_article()
3523                )));
3524            };
3525
3526            match owner_segment {
3527                Segment::ControlPointSpline(spline) => {
3528                    let Some(index) = spline
3529                        .controls
3530                        .windows(2)
3531                        .position(|window| window[0] == line.start && window[1] == line.end)
3532                    else {
3533                        return Err(KclError::refactor(format!(
3534                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3535                        )));
3536                    };
3537                    let owner_expr =
3538                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3539                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3540                    Ok(create_index_expression(edges_expr, index))
3541                }
3542                _ => Err(KclError::refactor(format!(
3543                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3544                ))),
3545            }
3546        } else {
3547            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3548        }
3549    }
3550
3551    fn coincident_segment_to_ast(
3552        &self,
3553        segment: &ConstraintSegment,
3554        new_ast: &mut ast::Node<ast::Program>,
3555    ) -> Result<ast::Expr, KclError> {
3556        match segment {
3557            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3558            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3559        }
3560    }
3561
3562    fn segment_id_to_constraint_ast_reference(
3563        &self,
3564        segment_id: ObjectId,
3565        new_ast: &mut ast::Node<ast::Program>,
3566    ) -> Result<ast::Expr, KclError> {
3567        let segment_object = self
3568            .scene_graph
3569            .objects
3570            .get(segment_id.0)
3571            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3572        let ObjectKind::Segment { segment } = &segment_object.kind else {
3573            return Err(KclError::refactor(format!(
3574                "Object is not a segment, it is {}",
3575                segment_object.kind.human_friendly_kind_with_article()
3576            )));
3577        };
3578
3579        match segment {
3580            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3581            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3582            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3583            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3584            Segment::ControlPointSpline(_) => {
3585                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3586            }
3587        }
3588    }
3589
3590    fn axis_constraint_segment_to_ast(
3591        &self,
3592        segment: &ConstraintSegment,
3593        new_ast: &mut ast::Node<ast::Program>,
3594    ) -> Result<ast::Expr, KclError> {
3595        match segment {
3596            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3597            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3598        }
3599    }
3600
3601    async fn add_coincident(
3602        &mut self,
3603        sketch: ObjectId,
3604        coincident: Coincident,
3605        new_ast: &mut ast::Node<ast::Program>,
3606    ) -> Result<AstNodeRef, KclError> {
3607        let sketch_id = sketch;
3608        for segment in &coincident.segments {
3609            let ConstraintSegment::Segment(segment_id) = segment else {
3610                continue;
3611            };
3612            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3613                continue;
3614            };
3615            if matches!(
3616                segment_object.kind,
3617                ObjectKind::Segment {
3618                    segment: Segment::ControlPointSpline(_)
3619                }
3620            ) {
3621                return Err(KclError::refactor(
3622                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3623                        .to_owned(),
3624                ));
3625            }
3626        }
3627        let segment_asts = coincident
3628            .segments
3629            .iter()
3630            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3631            .collect::<Result<Vec<_>, _>>()?;
3632        if segment_asts.len() < 2 {
3633            return Err(KclError::refactor(format!(
3634                "Coincident constraint must have at least 2 inputs, got {}",
3635                segment_asts.len()
3636            )));
3637        }
3638
3639        // Create the coincident() call using shared helper.
3640        let coincident_ast = create_coincident_ast(segment_asts);
3641
3642        // Add the line to the AST of the sketch block.
3643        let (sketch_block_ref, _) = self.mutate_ast(
3644            new_ast,
3645            sketch_id,
3646            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3647        )?;
3648        Ok(sketch_block_ref)
3649    }
3650
3651    async fn add_distance(
3652        &mut self,
3653        sketch: ObjectId,
3654        distance: Distance,
3655        new_ast: &mut ast::Node<ast::Program>,
3656    ) -> Result<AstNodeRef, KclError> {
3657        let sketch_id = sketch;
3658        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3659            [pt0, pt1] => [
3660                self.coincident_segment_to_ast(pt0, new_ast)?,
3661                self.coincident_segment_to_ast(pt1, new_ast)?,
3662            ],
3663            _ => {
3664                return Err(KclError::refactor(format!(
3665                    "Distance constraint must have exactly 2 points, got {}",
3666                    distance.points.len()
3667                )));
3668            }
3669        };
3670
3671        let arguments = match &distance.label_position {
3672            Some(label_position) => vec![ast::LabeledArg {
3673                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3674                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3675            }],
3676            None => Default::default(),
3677        };
3678
3679        // Create the distance() call.
3680        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3681            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3682            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3683                ast::ArrayExpression {
3684                    elements: vec![pt0_ast, pt1_ast],
3685                    digest: None,
3686                    non_code_meta: Default::default(),
3687                },
3688            )))),
3689            arguments,
3690            digest: None,
3691            non_code_meta: Default::default(),
3692        })));
3693        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3694            left: distance_call_ast,
3695            operator: ast::BinaryOperator::Eq,
3696            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3697                value: ast::LiteralValue::Number {
3698                    value: distance.distance.value,
3699                    suffix: distance.distance.units,
3700                },
3701                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3702                    KclError::refactor(format!(
3703                        "Could not format numeric suffix: {:?}",
3704                        distance.distance.units
3705                    ))
3706                })?,
3707                digest: None,
3708            }))),
3709            digest: None,
3710        })));
3711
3712        // Add the line to the AST of the sketch block.
3713        let (sketch_block_ref, _) = self.mutate_ast(
3714            new_ast,
3715            sketch_id,
3716            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3717        )?;
3718        Ok(sketch_block_ref)
3719    }
3720
3721    async fn add_angle(
3722        &mut self,
3723        sketch: ObjectId,
3724        angle: Angle,
3725        new_ast: &mut ast::Node<ast::Program>,
3726    ) -> Result<AstNodeRef, KclError> {
3727        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3728            return Err(KclError::refactor(format!(
3729                "Angle constraint must have exactly 2 lines, got {}",
3730                angle.lines.len()
3731            )));
3732        };
3733        let sketch_id = sketch;
3734
3735        // Map the runtime objects back to variable names.
3736        let line0_object = self
3737            .scene_graph
3738            .objects
3739            .get(l0_id.0)
3740            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3741        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3742            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3743        };
3744        let Segment::Line(_) = line0_segment else {
3745            return Err(KclError::refactor(format!(
3746                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3747            )));
3748        };
3749        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3750
3751        let line1_object = self
3752            .scene_graph
3753            .objects
3754            .get(l1_id.0)
3755            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3756        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3757            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3758        };
3759        let Segment::Line(_) = line1_segment else {
3760            return Err(KclError::refactor(format!(
3761                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3762            )));
3763        };
3764        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3765
3766        // Create the angle() call.
3767        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3768            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3769            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3770                ast::ArrayExpression {
3771                    elements: vec![l0_ast, l1_ast],
3772                    digest: None,
3773                    non_code_meta: Default::default(),
3774                },
3775            )))),
3776            arguments: Default::default(),
3777            digest: None,
3778            non_code_meta: Default::default(),
3779        })));
3780        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3781            left: angle_call_ast,
3782            operator: ast::BinaryOperator::Eq,
3783            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3784                value: ast::LiteralValue::Number {
3785                    value: angle.angle.value,
3786                    suffix: angle.angle.units,
3787                },
3788                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3789                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3790                })?,
3791                digest: None,
3792            }))),
3793            digest: None,
3794        })));
3795
3796        // Add the line to the AST of the sketch block.
3797        let (sketch_block_ref, _) = self.mutate_ast(
3798            new_ast,
3799            sketch_id,
3800            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3801        )?;
3802        Ok(sketch_block_ref)
3803    }
3804
3805    async fn add_tangent(
3806        &mut self,
3807        sketch: ObjectId,
3808        tangent: Tangent,
3809        new_ast: &mut ast::Node<ast::Program>,
3810    ) -> Result<AstNodeRef, KclError> {
3811        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3812            return Err(KclError::refactor(format!(
3813                "Tangent constraint must have exactly 2 segments, got {}",
3814                tangent.input.len()
3815            )));
3816        };
3817        let sketch_id = sketch;
3818
3819        let seg0_object = self
3820            .scene_graph
3821            .objects
3822            .get(seg0_id.0)
3823            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3824        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3825            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3826        };
3827        let seg0_ast = match seg0_segment {
3828            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3829                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3830            }
3831            _ => {
3832                return Err(KclError::refactor(format!(
3833                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3834                )));
3835            }
3836        };
3837
3838        let seg1_object = self
3839            .scene_graph
3840            .objects
3841            .get(seg1_id.0)
3842            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3843        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3844            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3845        };
3846        let seg1_ast = match seg1_segment {
3847            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3848                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3849            }
3850            _ => {
3851                return Err(KclError::refactor(format!(
3852                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3853                )));
3854            }
3855        };
3856
3857        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3858        let (sketch_block_ref, _) = self.mutate_ast(
3859            new_ast,
3860            sketch_id,
3861            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3862        )?;
3863        Ok(sketch_block_ref)
3864    }
3865
3866    async fn add_symmetric(
3867        &mut self,
3868        sketch: ObjectId,
3869        symmetric: Symmetric,
3870        new_ast: &mut ast::Node<ast::Program>,
3871    ) -> Result<AstNodeRef, KclError> {
3872        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3873            return Err(KclError::refactor(format!(
3874                "Symmetric constraint must have exactly 2 inputs, got {}",
3875                symmetric.input.len()
3876            )));
3877        };
3878        let sketch_id = sketch;
3879
3880        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3881        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3882        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3883
3884        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3885        let (sketch_block_ref, _) = self.mutate_ast(
3886            new_ast,
3887            sketch_id,
3888            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3889        )?;
3890        Ok(sketch_block_ref)
3891    }
3892
3893    async fn add_midpoint(
3894        &mut self,
3895        sketch: ObjectId,
3896        midpoint: Midpoint,
3897        new_ast: &mut ast::Node<ast::Program>,
3898    ) -> Result<AstNodeRef, KclError> {
3899        let sketch_id = sketch;
3900        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3901
3902        let segment_object = self
3903            .scene_graph
3904            .objects
3905            .get(midpoint.segment.0)
3906            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3907        let ObjectKind::Segment {
3908            segment: midpoint_segment,
3909        } = &segment_object.kind
3910        else {
3911            return Err(KclError::refactor(format!(
3912                "Object must be a segment, but it was {}",
3913                segment_object.kind.human_friendly_kind_with_article()
3914            )));
3915        };
3916        let segment_ast = match midpoint_segment {
3917            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3918            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3919            _ => {
3920                return Err(KclError::refactor(format!(
3921                    "Midpoint target must be a line or arc segment but it was {}",
3922                    midpoint_segment.human_friendly_kind_with_article()
3923                )));
3924            }
3925        };
3926
3927        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3928        let (sketch_block_ref, _) = self.mutate_ast(
3929            new_ast,
3930            sketch_id,
3931            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3932        )?;
3933        Ok(sketch_block_ref)
3934    }
3935
3936    async fn add_equal_radius(
3937        &mut self,
3938        sketch: ObjectId,
3939        equal_radius: EqualRadius,
3940        new_ast: &mut ast::Node<ast::Program>,
3941    ) -> Result<AstNodeRef, KclError> {
3942        if equal_radius.input.len() < 2 {
3943            return Err(KclError::refactor(format!(
3944                "equalRadius constraint must have at least 2 segments, got {}",
3945                equal_radius.input.len()
3946            )));
3947        }
3948
3949        let sketch_id = sketch;
3950        let input_asts = equal_radius
3951            .input
3952            .iter()
3953            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3954            .collect::<Result<Vec<_>, _>>()?;
3955
3956        let equal_radius_ast = create_equal_radius_ast(input_asts);
3957        let (sketch_block_ref, _) = self.mutate_ast(
3958            new_ast,
3959            sketch_id,
3960            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3961        )?;
3962        Ok(sketch_block_ref)
3963    }
3964
3965    async fn add_radius(
3966        &mut self,
3967        sketch: ObjectId,
3968        radius: Radius,
3969        new_ast: &mut ast::Node<ast::Program>,
3970    ) -> Result<AstNodeRef, KclError> {
3971        let params = ArcSizeConstraintParams {
3972            points: vec![radius.arc],
3973            function_name: RADIUS_FN,
3974            value: radius.radius.value,
3975            units: radius.radius.units,
3976            label_position: radius.label_position,
3977            constraint_type_name: "Radius",
3978        };
3979        self.add_arc_size_constraint(sketch, params, new_ast).await
3980    }
3981
3982    async fn add_diameter(
3983        &mut self,
3984        sketch: ObjectId,
3985        diameter: Diameter,
3986        new_ast: &mut ast::Node<ast::Program>,
3987    ) -> Result<AstNodeRef, KclError> {
3988        let params = ArcSizeConstraintParams {
3989            points: vec![diameter.arc],
3990            function_name: DIAMETER_FN,
3991            value: diameter.diameter.value,
3992            units: diameter.diameter.units,
3993            label_position: diameter.label_position,
3994            constraint_type_name: "Diameter",
3995        };
3996        self.add_arc_size_constraint(sketch, params, new_ast).await
3997    }
3998
3999    async fn add_fixed_constraints(
4000        &mut self,
4001        sketch: ObjectId,
4002        points: Vec<FixedPoint>,
4003        new_ast: &mut ast::Node<ast::Program>,
4004    ) -> Result<AstNodeRef, KclError> {
4005        let mut sketch_block_ref = None;
4006
4007        for fixed_point in points {
4008            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4009            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4010                .map_err(|err| KclError::refactor(err.to_string()))?;
4011
4012            let (sketch_ref, _) = self.mutate_ast(
4013                new_ast,
4014                sketch,
4015                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4016            )?;
4017            sketch_block_ref = Some(sketch_ref);
4018        }
4019
4020        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4021    }
4022
4023    async fn add_arc_size_constraint(
4024        &mut self,
4025        sketch: ObjectId,
4026        params: ArcSizeConstraintParams,
4027        new_ast: &mut ast::Node<ast::Program>,
4028    ) -> Result<AstNodeRef, KclError> {
4029        let sketch_id = sketch;
4030
4031        // Constraint must have exactly 1 argument (arc segment)
4032        if params.points.len() != 1 {
4033            return Err(KclError::refactor(format!(
4034                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4035                params.constraint_type_name,
4036                params.points.len()
4037            )));
4038        }
4039
4040        let arc_id = params.points[0];
4041        let arc_object = self
4042            .scene_graph
4043            .objects
4044            .get(arc_id.0)
4045            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4046        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4047            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4048        };
4049        let ref_type = match arc_segment {
4050            Segment::Arc(_) => ARC_VARIABLE,
4051            Segment::Circle(_) => CIRCLE_VARIABLE,
4052            _ => {
4053                return Err(KclError::refactor(format!(
4054                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4055                    params.constraint_type_name
4056                )));
4057            }
4058        };
4059        // Reference the arc/circle segment directly
4060        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4061        let arguments = match &params.label_position {
4062            Some(label_position) => vec![ast::LabeledArg {
4063                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4064                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4065            }],
4066            None => Default::default(),
4067        };
4068
4069        // Create the function call.
4070        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4071            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4072            unlabeled: Some(arc_ast),
4073            arguments,
4074            digest: None,
4075            non_code_meta: Default::default(),
4076        })));
4077        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4078            left: call_ast,
4079            operator: ast::BinaryOperator::Eq,
4080            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4081                value: ast::LiteralValue::Number {
4082                    value: params.value,
4083                    suffix: params.units,
4084                },
4085                raw: format_number_literal(params.value, params.units, None)
4086                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4087                digest: None,
4088            }))),
4089            digest: None,
4090        })));
4091
4092        // Add the line to the AST of the sketch block.
4093        let (sketch_block_ref, _) = self.mutate_ast(
4094            new_ast,
4095            sketch_id,
4096            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4097        )?;
4098        Ok(sketch_block_ref)
4099    }
4100
4101    async fn add_horizontal_distance(
4102        &mut self,
4103        sketch: ObjectId,
4104        distance: Distance,
4105        new_ast: &mut ast::Node<ast::Program>,
4106    ) -> Result<AstNodeRef, KclError> {
4107        let sketch_id = sketch;
4108        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4109            [pt0, pt1] => [
4110                self.coincident_segment_to_ast(pt0, new_ast)?,
4111                self.coincident_segment_to_ast(pt1, new_ast)?,
4112            ],
4113            _ => {
4114                return Err(KclError::refactor(format!(
4115                    "Horizontal distance constraint must have exactly 2 points, got {}",
4116                    distance.points.len()
4117                )));
4118            }
4119        };
4120
4121        let arguments = match &distance.label_position {
4122            Some(label_position) => vec![ast::LabeledArg {
4123                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4124                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4125            }],
4126            None => Default::default(),
4127        };
4128
4129        // Create the horizontalDistance() call.
4130        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4131            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4132            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4133                ast::ArrayExpression {
4134                    elements: vec![pt0_ast, pt1_ast],
4135                    digest: None,
4136                    non_code_meta: Default::default(),
4137                },
4138            )))),
4139            arguments,
4140            digest: None,
4141            non_code_meta: Default::default(),
4142        })));
4143        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4144            left: distance_call_ast,
4145            operator: ast::BinaryOperator::Eq,
4146            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4147                value: ast::LiteralValue::Number {
4148                    value: distance.distance.value,
4149                    suffix: distance.distance.units,
4150                },
4151                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4152                    KclError::refactor(format!(
4153                        "Could not format numeric suffix: {:?}",
4154                        distance.distance.units
4155                    ))
4156                })?,
4157                digest: None,
4158            }))),
4159            digest: None,
4160        })));
4161
4162        // Add the line to the AST of the sketch block.
4163        let (sketch_block_ref, _) = self.mutate_ast(
4164            new_ast,
4165            sketch_id,
4166            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4167        )?;
4168        Ok(sketch_block_ref)
4169    }
4170
4171    async fn add_vertical_distance(
4172        &mut self,
4173        sketch: ObjectId,
4174        distance: Distance,
4175        new_ast: &mut ast::Node<ast::Program>,
4176    ) -> Result<AstNodeRef, KclError> {
4177        let sketch_id = sketch;
4178        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4179            [pt0, pt1] => [
4180                self.coincident_segment_to_ast(pt0, new_ast)?,
4181                self.coincident_segment_to_ast(pt1, new_ast)?,
4182            ],
4183            _ => {
4184                return Err(KclError::refactor(format!(
4185                    "Vertical distance constraint must have exactly 2 points, got {}",
4186                    distance.points.len()
4187                )));
4188            }
4189        };
4190
4191        let arguments = match &distance.label_position {
4192            Some(label_position) => vec![ast::LabeledArg {
4193                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4194                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4195            }],
4196            None => Default::default(),
4197        };
4198
4199        // Create the verticalDistance() call.
4200        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4201            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4202            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4203                ast::ArrayExpression {
4204                    elements: vec![pt0_ast, pt1_ast],
4205                    digest: None,
4206                    non_code_meta: Default::default(),
4207                },
4208            )))),
4209            arguments,
4210            digest: None,
4211            non_code_meta: Default::default(),
4212        })));
4213        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4214            left: distance_call_ast,
4215            operator: ast::BinaryOperator::Eq,
4216            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4217                value: ast::LiteralValue::Number {
4218                    value: distance.distance.value,
4219                    suffix: distance.distance.units,
4220                },
4221                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4222                    KclError::refactor(format!(
4223                        "Could not format numeric suffix: {:?}",
4224                        distance.distance.units
4225                    ))
4226                })?,
4227                digest: None,
4228            }))),
4229            digest: None,
4230        })));
4231
4232        // Add the line to the AST of the sketch block.
4233        let (sketch_block_ref, _) = self.mutate_ast(
4234            new_ast,
4235            sketch_id,
4236            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4237        )?;
4238        Ok(sketch_block_ref)
4239    }
4240
4241    async fn add_horizontal(
4242        &mut self,
4243        sketch: ObjectId,
4244        horizontal: Horizontal,
4245        new_ast: &mut ast::Node<ast::Program>,
4246    ) -> Result<AstNodeRef, KclError> {
4247        let sketch_id = sketch;
4248
4249        // Map the runtime objects back to variable names.
4250        let first_arg_ast = match horizontal {
4251            Horizontal::Line { line } => {
4252                let line_object = self
4253                    .scene_graph
4254                    .objects
4255                    .get(line.0)
4256                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4257                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4258                    let kind = line_object.kind.human_friendly_kind_with_article();
4259                    return Err(KclError::refactor(format!(
4260                        "This constraint only works on Segments, but you selected {kind}"
4261                    )));
4262                };
4263                let Segment::Line(_) = line_segment else {
4264                    return Err(KclError::refactor(format!(
4265                        "Only lines can be made horizontal, but you selected {}",
4266                        line_segment.human_friendly_kind_with_article(),
4267                    )));
4268                };
4269                self.line_id_to_ast_reference(line, new_ast)?
4270            }
4271            Horizontal::Points { points } => {
4272                let point_asts = points
4273                    .iter()
4274                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4275                    .collect::<Result<Vec<_>, _>>()?;
4276                ast::ArrayExpression::new(point_asts).into()
4277            }
4278        };
4279        // Create the horizontal() call using shared helper.
4280        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4281
4282        // Add the line to the AST of the sketch block.
4283        let (sketch_block_ref, _) = self.mutate_ast(
4284            new_ast,
4285            sketch_id,
4286            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4287        )?;
4288        Ok(sketch_block_ref)
4289    }
4290
4291    async fn add_lines_equal_length(
4292        &mut self,
4293        sketch: ObjectId,
4294        lines_equal_length: LinesEqualLength,
4295        new_ast: &mut ast::Node<ast::Program>,
4296    ) -> Result<AstNodeRef, KclError> {
4297        if lines_equal_length.lines.len() < 2 {
4298            return Err(KclError::refactor(format!(
4299                "Lines equal length constraint must have at least 2 lines, got {}",
4300                lines_equal_length.lines.len()
4301            )));
4302        };
4303
4304        let sketch_id = sketch;
4305
4306        // Map the runtime objects back to variable names.
4307        let line_asts = lines_equal_length
4308            .lines
4309            .iter()
4310            .map(|line_id| {
4311                let line_object = self
4312                    .scene_graph
4313                    .objects
4314                    .get(line_id.0)
4315                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4316                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4317                    let kind = line_object.kind.human_friendly_kind_with_article();
4318                    return Err(KclError::refactor(format!(
4319                        "This constraint only works on Segments, but you selected {kind}"
4320                    )));
4321                };
4322                let Segment::Line(_) = line_segment else {
4323                    let kind = line_segment.human_friendly_kind_with_article();
4324                    return Err(KclError::refactor(format!(
4325                        "Only lines can be made equal length, but you selected {kind}"
4326                    )));
4327                };
4328
4329                self.line_id_to_ast_reference(*line_id, new_ast)
4330            })
4331            .collect::<Result<Vec<_>, _>>()?;
4332
4333        // Create the equalLength() call using shared helper.
4334        let equal_length_ast = create_equal_length_ast(line_asts);
4335
4336        // Add the constraint to the AST of the sketch block.
4337        let (sketch_block_ref, _) = self.mutate_ast(
4338            new_ast,
4339            sketch_id,
4340            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4341        )?;
4342        Ok(sketch_block_ref)
4343    }
4344
4345    fn equal_radius_segment_id_to_ast_reference(
4346        &mut self,
4347        segment_id: ObjectId,
4348        new_ast: &mut ast::Node<ast::Program>,
4349    ) -> Result<ast::Expr, KclError> {
4350        let segment_object = self
4351            .scene_graph
4352            .objects
4353            .get(segment_id.0)
4354            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4355        let ObjectKind::Segment { segment } = &segment_object.kind else {
4356            return Err(KclError::refactor(format!(
4357                "Object is not a segment, it was {}",
4358                segment_object.kind.human_friendly_kind_with_article()
4359            )));
4360        };
4361
4362        let ref_type = match segment {
4363            Segment::Arc(_) => ARC_VARIABLE,
4364            Segment::Circle(_) => CIRCLE_VARIABLE,
4365            _ => {
4366                return Err(KclError::refactor(format!(
4367                    "equalRadius supports only arc/circle segments, got {}",
4368                    segment.human_friendly_kind_with_article()
4369                )));
4370            }
4371        };
4372
4373        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4374    }
4375
4376    fn symmetric_input_id_to_ast_reference(
4377        &mut self,
4378        segment_id: ObjectId,
4379        new_ast: &mut ast::Node<ast::Program>,
4380    ) -> Result<ast::Expr, KclError> {
4381        let segment_object = self
4382            .scene_graph
4383            .objects
4384            .get(segment_id.0)
4385            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4386        let ObjectKind::Segment { segment } = &segment_object.kind else {
4387            return Err(KclError::refactor(format!(
4388                "Object is not a segment, it was {}",
4389                segment_object.kind.human_friendly_kind_with_article()
4390            )));
4391        };
4392
4393        match segment {
4394            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4395            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4396            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4397            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4398            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4399                "Symmetric does not yet support control point splines".to_owned(),
4400            )),
4401        }
4402    }
4403
4404    fn symmetric_axis_id_to_ast_reference(
4405        &mut self,
4406        segment_id: ObjectId,
4407        new_ast: &mut ast::Node<ast::Program>,
4408    ) -> Result<ast::Expr, KclError> {
4409        let segment_object = self
4410            .scene_graph
4411            .objects
4412            .get(segment_id.0)
4413            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4414        let ObjectKind::Segment { segment } = &segment_object.kind else {
4415            return Err(KclError::refactor(format!(
4416                "Object is not a segment, it was {}",
4417                segment_object.kind.human_friendly_kind_with_article()
4418            )));
4419        };
4420        match segment {
4421            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4422            _ => Err(KclError::refactor(format!(
4423                "Symmetric axis must be a line, got {}",
4424                segment.human_friendly_kind_with_article()
4425            ))),
4426        }
4427    }
4428
4429    async fn add_parallel(
4430        &mut self,
4431        sketch: ObjectId,
4432        parallel: Parallel,
4433        new_ast: &mut ast::Node<ast::Program>,
4434    ) -> Result<AstNodeRef, KclError> {
4435        if parallel.lines.len() < 2 {
4436            return Err(KclError::refactor(format!(
4437                "Parallel constraint must have at least 2 lines, got {}",
4438                parallel.lines.len()
4439            )));
4440        };
4441
4442        let sketch_id = sketch;
4443
4444        let line_asts = parallel
4445            .lines
4446            .iter()
4447            .map(|line_id| {
4448                let line_object = self
4449                    .scene_graph
4450                    .objects
4451                    .get(line_id.0)
4452                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4453                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4454                    let kind = line_object.kind.human_friendly_kind_with_article();
4455                    return Err(KclError::refactor(format!(
4456                        "This constraint only works on Segments, but you selected {kind}"
4457                    )));
4458                };
4459                let Segment::Line(_) = line_segment else {
4460                    let kind = line_segment.human_friendly_kind_with_article();
4461                    return Err(KclError::refactor(format!(
4462                        "Only lines can be made parallel, but you selected {kind}"
4463                    )));
4464                };
4465
4466                self.line_id_to_ast_reference(*line_id, new_ast)
4467            })
4468            .collect::<Result<Vec<_>, _>>()?;
4469
4470        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4471            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4472            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4473                ast::ArrayExpression {
4474                    elements: line_asts,
4475                    digest: None,
4476                    non_code_meta: Default::default(),
4477                },
4478            )))),
4479            arguments: Default::default(),
4480            digest: None,
4481            non_code_meta: Default::default(),
4482        })));
4483
4484        let (sketch_block_ref, _) = self.mutate_ast(
4485            new_ast,
4486            sketch_id,
4487            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4488        )?;
4489        Ok(sketch_block_ref)
4490    }
4491
4492    async fn add_perpendicular(
4493        &mut self,
4494        sketch: ObjectId,
4495        perpendicular: Perpendicular,
4496        new_ast: &mut ast::Node<ast::Program>,
4497    ) -> Result<AstNodeRef, KclError> {
4498        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4499            .await
4500    }
4501
4502    async fn add_lines_at_angle_constraint(
4503        &mut self,
4504        sketch: ObjectId,
4505        angle_kind: LinesAtAngleKind,
4506        lines: Vec<ObjectId>,
4507        new_ast: &mut ast::Node<ast::Program>,
4508    ) -> Result<AstNodeRef, KclError> {
4509        let &[line0_id, line1_id] = lines.as_slice() else {
4510            return Err(KclError::refactor(format!(
4511                "{} constraint must have exactly 2 lines, got {}",
4512                angle_kind.to_function_name(),
4513                lines.len()
4514            )));
4515        };
4516
4517        let sketch_id = sketch;
4518
4519        // Map the runtime objects back to variable names.
4520        let line0_object = self
4521            .scene_graph
4522            .objects
4523            .get(line0_id.0)
4524            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4525        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4526            let kind = line0_object.kind.human_friendly_kind_with_article();
4527            return Err(KclError::refactor(format!(
4528                "This constraint only works on Segments, but you selected {kind}"
4529            )));
4530        };
4531        let Segment::Line(_) = line0_segment else {
4532            return Err(KclError::refactor(format!(
4533                "Only lines can be made {}, but you selected {}",
4534                angle_kind.to_function_name(),
4535                line0_segment.human_friendly_kind_with_article(),
4536            )));
4537        };
4538        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4539
4540        let line1_object = self
4541            .scene_graph
4542            .objects
4543            .get(line1_id.0)
4544            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4545        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4546            let kind = line1_object.kind.human_friendly_kind_with_article();
4547            return Err(KclError::refactor(format!(
4548                "This constraint only works on Segments, but you selected {kind}"
4549            )));
4550        };
4551        let Segment::Line(_) = line1_segment else {
4552            return Err(KclError::refactor(format!(
4553                "Only lines can be made {}, but you selected {}",
4554                angle_kind.to_function_name(),
4555                line1_segment.human_friendly_kind_with_article(),
4556            )));
4557        };
4558        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4559
4560        // Create the parallel() or perpendicular() call.
4561        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4562            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4563            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4564                ast::ArrayExpression {
4565                    elements: vec![line0_ast, line1_ast],
4566                    digest: None,
4567                    non_code_meta: Default::default(),
4568                },
4569            )))),
4570            arguments: Default::default(),
4571            digest: None,
4572            non_code_meta: Default::default(),
4573        })));
4574
4575        // Add the constraint to the AST of the sketch block.
4576        let (sketch_block_ref, _) = self.mutate_ast(
4577            new_ast,
4578            sketch_id,
4579            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4580        )?;
4581        Ok(sketch_block_ref)
4582    }
4583
4584    async fn add_vertical(
4585        &mut self,
4586        sketch: ObjectId,
4587        vertical: Vertical,
4588        new_ast: &mut ast::Node<ast::Program>,
4589    ) -> Result<AstNodeRef, KclError> {
4590        let sketch_id = sketch;
4591
4592        let first_arg_ast = match vertical {
4593            Vertical::Line { line } => {
4594                // Map the runtime objects back to variable names.
4595                let line_object = self
4596                    .scene_graph
4597                    .objects
4598                    .get(line.0)
4599                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4600                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4601                    let kind = line_object.kind.human_friendly_kind_with_article();
4602                    return Err(KclError::refactor(format!(
4603                        "This constraint only works on Segments, but you selected {kind}"
4604                    )));
4605                };
4606                let Segment::Line(_) = line_segment else {
4607                    return Err(KclError::refactor(format!(
4608                        "Only lines can be made vertical, but you selected {}",
4609                        line_segment.human_friendly_kind_with_article()
4610                    )));
4611                };
4612                self.line_id_to_ast_reference(line, new_ast)?
4613            }
4614            Vertical::Points { points } => {
4615                let point_asts = points
4616                    .iter()
4617                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4618                    .collect::<Result<Vec<_>, _>>()?;
4619                ast::ArrayExpression::new(point_asts).into()
4620            }
4621        };
4622        // Create the vertical() call using shared helper.
4623        let vertical_ast = create_vertical_ast(first_arg_ast);
4624
4625        // Add the line to the AST of the sketch block.
4626        let (sketch_block_ref, _) = self.mutate_ast(
4627            new_ast,
4628            sketch_id,
4629            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4630        )?;
4631        Ok(sketch_block_ref)
4632    }
4633
4634    async fn execute_after_add_constraint(
4635        &mut self,
4636        ctx: &ExecutorContext,
4637        sketch_id: ObjectId,
4638        sketch_block_ref: AstNodeRef,
4639        new_ast: &mut ast::Node<ast::Program>,
4640    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4641        // Convert to string source to create real source ranges.
4642        let new_source = source_from_ast(new_ast);
4643        // Parse the new KCL source.
4644        let (new_program, errors) = Program::parse(&new_source)
4645            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4646        if !errors.is_empty() {
4647            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4648                "Error parsing KCL source after adding constraint: {errors:?}"
4649            ))));
4650        }
4651        let Some(new_program) = new_program else {
4652            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4653                "No AST produced after adding constraint".to_string(),
4654            )));
4655        };
4656        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4657            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4658                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4659            )))
4660        })?;
4661
4662        // Truncate after the sketch block for mock execution.
4663        // Use a clone so we don't mutate new_program yet
4664        let mut truncated_program = new_program.clone();
4665        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4666            .map_err(KclErrorWithOutputs::no_outputs)?;
4667
4668        // Execute - if this fails, we haven't modified self yet, so state is safe
4669        let outcome = ctx
4670            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4671            .await?;
4672
4673        let new_object_ids = {
4674            // Extract the constraint ID from the execution outcome using source_range_to_object
4675            let constraint_id = outcome
4676                .source_range_to_object
4677                .get(&constraint_node_ref.range)
4678                .copied()
4679                .ok_or_else(|| {
4680                    KclErrorWithOutputs::from_error_outcome(
4681                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4682                        outcome.clone(),
4683                    )
4684                })?;
4685            vec![constraint_id]
4686        };
4687
4688        // Only now, after all operations succeeded, update self.program.
4689        // This ensures state is only modified if everything succeeds.
4690        self.program = new_program;
4691
4692        // Uses MockConfig::default() which has freedom_analysis: true
4693        let outcome = self.update_state_after_exec(outcome, true);
4694
4695        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4696        let scene_graph_delta = SceneGraphDelta {
4697            new_graph: self.scene_graph_for_ui(),
4698            invalidates_ids: false,
4699            new_objects: new_object_ids,
4700            exec_outcome: outcome,
4701        };
4702        Ok((src_delta, scene_graph_delta))
4703    }
4704
4705    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4706        let commit_failure = || {
4707            KclErrorWithOutputs::from_error_outcome(
4708                KclError::refactor(format!("Could not update KCL after {operation}.")),
4709                outcome.clone(),
4710            )
4711        };
4712
4713        let default_length_unit = self.default_length_unit();
4714        let mut settled_ast = self.program.ast.clone();
4715        let mut committed_solver_value = false;
4716        for (var_range, node_path, value) in &outcome.var_solutions {
4717            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4718                return Err(commit_failure());
4719            };
4720            let new_value = match &lookup {
4721                Some(current_literal) => {
4722                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4723                        continue;
4724                    }
4725                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4726                }
4727                None => {
4728                    // Bare `var` with no initial literal to compare against;
4729                    // always commit, using the module's default length unit as
4730                    // an explicit suffix so the written value carries units.
4731                    Number {
4732                        value: number_value_in_default_length_units(*value, default_length_unit),
4733                        units: default_length_unit.into(),
4734                    }
4735                }
4736            };
4737            committed_solver_value = true;
4738            let source_ref = SourceRef::Simple {
4739                range: *var_range,
4740                node_path: node_path.clone(),
4741            };
4742            mutate_ast_node_by_source_ref(
4743                &mut settled_ast,
4744                &source_ref,
4745                AstMutateCommand::EditVarInitialValue { value: new_value },
4746            )
4747            .map_err(|_| commit_failure())?;
4748        }
4749
4750        if !committed_solver_value {
4751            return Ok(SourceDelta {
4752                text: self.program.original_file_contents.clone(),
4753            });
4754        }
4755
4756        let settled_source = source_from_ast(&settled_ast);
4757        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4758        if !errors.is_empty() {
4759            return Err(commit_failure());
4760        }
4761        let Some(settled_program) = settled_program else {
4762            return Err(commit_failure());
4763        };
4764
4765        self.program = settled_program;
4766
4767        Ok(SourceDelta { text: settled_source })
4768    }
4769
4770    // Find constraints that reference the given segments.
4771    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4772        if segment_ids_set.contains(&segment_id) {
4773            return true;
4774        }
4775
4776        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4777            return false;
4778        };
4779        let ObjectKind::Segment { segment } = &segment_object.kind else {
4780            return false;
4781        };
4782        let Segment::Point(point) = segment else {
4783            return false;
4784        };
4785
4786        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4787    }
4788
4789    fn remaining_constraint_segments(
4790        &self,
4791        segments: &[ConstraintSegment],
4792        segment_ids_set: &AhashIndexSet<ObjectId>,
4793    ) -> Vec<ConstraintSegment> {
4794        segments
4795            .iter()
4796            .copied()
4797            .filter(|segment| match segment {
4798                ConstraintSegment::Origin(_) => true,
4799                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4800            })
4801            .collect()
4802    }
4803
4804    fn find_referenced_constraints(
4805        &self,
4806        sketch_id: ObjectId,
4807        segment_ids_set: &AhashIndexSet<ObjectId>,
4808    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4809        // Look up the sketch.
4810        let sketch_object = self
4811            .scene_graph
4812            .objects
4813            .get(sketch_id.0)
4814            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4815        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4816            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4817        };
4818        let segment_or_owner_matches = |segment_id: ObjectId| {
4819            if segment_ids_set.contains(&segment_id) {
4820                return true;
4821            }
4822            let segment_object = self.scene_graph.objects.get(segment_id.0);
4823            if let Some(obj) = segment_object
4824                && let ObjectKind::Segment { segment } = &obj.kind
4825            {
4826                match segment {
4827                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4828                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4829                    _ => false,
4830                }
4831            } else {
4832                false
4833            }
4834        };
4835        let mut constraint_ids_set = AhashIndexSet::default();
4836        for constraint_id in &sketch.constraints {
4837            let constraint_object = self
4838                .scene_graph
4839                .objects
4840                .get(constraint_id.0)
4841                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4842            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4843                return Err(KclError::refactor(format!(
4844                    "Object is not a constraint, it is {}",
4845                    constraint_object.kind.human_friendly_kind_with_article()
4846                )));
4847            };
4848            let depends_on_segment = match constraint {
4849                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4850                Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4851                Constraint::Fixed(fixed) => fixed
4852                    .points
4853                    .iter()
4854                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4855                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4856                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4857                Constraint::EqualRadius(equal_radius) => {
4858                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4859                }
4860                Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4861                Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4862                Constraint::Horizontal(h) => match h {
4863                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4864                    Horizontal::Points { points } => points.iter().any(|point| match point {
4865                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4866                        ConstraintSegment::Origin(_) => false,
4867                    }),
4868                },
4869                Constraint::Vertical(v) => match v {
4870                    Vertical::Line { line } => segment_or_owner_matches(*line),
4871                    Vertical::Points { points } => points.iter().any(|point| match point {
4872                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4873                        ConstraintSegment::Origin(_) => false,
4874                    }),
4875                },
4876                Constraint::LinesEqualLength(lines_equal_length) => {
4877                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4878                }
4879                Constraint::Midpoint(midpoint) => {
4880                    segment_or_owner_matches(midpoint.segment)
4881                        || matches!(
4882                            midpoint.point,
4883                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4884                        )
4885                }
4886                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4887                Constraint::Perpendicular(perpendicular) => {
4888                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4889                }
4890                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4891                Constraint::Symmetric(symmetric) => {
4892                    segment_or_owner_matches(symmetric.axis)
4893                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4894                }
4895                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4896            };
4897            if depends_on_segment {
4898                constraint_ids_set.insert(*constraint_id);
4899            }
4900        }
4901        Ok(constraint_ids_set)
4902    }
4903
4904    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4905        let mut outcome = outcome;
4906        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4907
4908        if freedom_analysis_ran {
4909            // When freedom analysis ran, replace the cache entirely with new values
4910            // Don't merge with old values since IDs might have changed
4911            self.point_freedom_cache.clear();
4912            for new_obj in &new_objects {
4913                if let ObjectKind::Segment {
4914                    segment: crate::front::Segment::Point(point),
4915                } = &new_obj.kind
4916                {
4917                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4918                }
4919            }
4920            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4921            // Objects are already correct from the analysis, just use them as-is
4922            self.scene_graph.objects = new_objects;
4923        } else {
4924            // When freedom analysis didn't run, preserve old values and merge
4925            // Before replacing objects, extract and store freedom values from old objects
4926            for old_obj in &self.scene_graph.objects {
4927                if let ObjectKind::Segment {
4928                    segment: crate::front::Segment::Point(point),
4929                } = &old_obj.kind
4930                {
4931                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4932                }
4933            }
4934
4935            // Update objects, preserving stored freedom values when new is Free (might be default)
4936            let mut updated_objects = Vec::with_capacity(new_objects.len());
4937            for new_obj in new_objects {
4938                let mut obj = new_obj;
4939                if let ObjectKind::Segment {
4940                    segment: crate::front::Segment::Point(point),
4941                } = &mut obj.kind
4942                {
4943                    let new_freedom = point.freedom;
4944                    // When freedom_analysis=false, new values are defaults (Free).
4945                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4946                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4947                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4948                    // when there are actually unsatisfied constraints.
4949                    match new_freedom {
4950                        Freedom::Free => {
4951                            match self.point_freedom_cache.get(&obj.id).copied() {
4952                                Some(Freedom::Conflict) => {
4953                                    // Don't preserve Conflict - conflicts are transient
4954                                    // Keep it as Free
4955                                }
4956                                Some(Freedom::Fixed) => {
4957                                    // Preserve Fixed cached value
4958                                    point.freedom = Freedom::Fixed;
4959                                }
4960                                Some(Freedom::Free) => {
4961                                    // If stored is also Free, keep Free (no change needed)
4962                                }
4963                                None => {
4964                                    // If no cached value, keep Free (default)
4965                                }
4966                            }
4967                        }
4968                        Freedom::Fixed => {
4969                            // Use new value (already set)
4970                        }
4971                        Freedom::Conflict => {
4972                            // Use new value (already set)
4973                        }
4974                    }
4975                    // Store the new freedom value (even if it's Free, so we know it was set)
4976                    self.point_freedom_cache.insert(obj.id, point.freedom);
4977                }
4978                updated_objects.push(obj);
4979            }
4980
4981            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4982            self.scene_graph.objects = updated_objects;
4983        }
4984        outcome
4985    }
4986
4987    fn mutate_ast(
4988        &mut self,
4989        ast: &mut ast::Node<ast::Program>,
4990        object_id: ObjectId,
4991        command: AstMutateCommand,
4992    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4993        let sketch_object = self
4994            .scene_graph
4995            .objects
4996            .get(object_id.0)
4997            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4998        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4999    }
5000}
5001
5002fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5003    // Look up existing sketch.
5004    let sketch_object = scene_graph
5005        .objects
5006        .get(sketch_id.0)
5007        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5008    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5009        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5010    };
5011    expect_single_node_ref(sketch_object)
5012}
5013
5014fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5015    match &object.source {
5016        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5017            range: *range,
5018            node_path: node_path.clone(),
5019        }),
5020        SourceRef::BackTrace { ranges } => {
5021            let [range] = ranges.as_slice() else {
5022                return Err(KclError::refactor(format!(
5023                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5024                    ranges.len()
5025                )));
5026            };
5027            Ok(AstNodeRef {
5028                range: range.0,
5029                node_path: range.1.clone(),
5030            })
5031        }
5032    }
5033}
5034
5035/// This is a deprecated fall-back implementation. Prefer
5036/// [`only_sketch_block()`] to avoid reliance on source ranges.
5037fn only_sketch_block_from_range(
5038    ast: &mut ast::Node<ast::Program>,
5039    sketch_block_range: SourceRange,
5040    edit_kind: ChangeKind,
5041) -> Result<(), KclError> {
5042    let r1 = sketch_block_range;
5043    let matches_range = |r2: SourceRange| -> bool {
5044        // We may have added items to the sketch block, so the end may not be an
5045        // exact match.
5046        match edit_kind {
5047            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5048            // For edit, we don't know whether it grew or shrank.
5049            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5050            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5051            // No edit should be an exact match.
5052            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5053        }
5054    };
5055    let mut found = false;
5056    for item in ast.body.iter_mut() {
5057        match item {
5058            ast::BodyItem::ImportStatement(_) => {}
5059            ast::BodyItem::ExpressionStatement(node) => {
5060                if matches_range(SourceRange::from(&*node))
5061                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5062                {
5063                    sketch_block.is_being_edited = true;
5064                    found = true;
5065                    break;
5066                }
5067            }
5068            ast::BodyItem::VariableDeclaration(node) => {
5069                if matches_range(SourceRange::from(&node.declaration.init))
5070                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5071                {
5072                    sketch_block.is_being_edited = true;
5073                    found = true;
5074                    break;
5075                }
5076            }
5077            ast::BodyItem::TypeDeclaration(_) => {}
5078            ast::BodyItem::ReturnStatement(node) => {
5079                if matches_range(SourceRange::from(&node.argument))
5080                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5081                {
5082                    sketch_block.is_being_edited = true;
5083                    found = true;
5084                    break;
5085                }
5086            }
5087        }
5088    }
5089    if !found {
5090        return Err(KclError::refactor(format!(
5091            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5092        )));
5093    }
5094
5095    Ok(())
5096}
5097
5098fn only_sketch_block(
5099    ast: &mut ast::Node<ast::Program>,
5100    sketch_block_ref: &AstNodeRef,
5101    edit_kind: ChangeKind,
5102) -> Result<(), KclError> {
5103    let Some(target_node_path) = &sketch_block_ref.node_path else {
5104        #[cfg(target_arch = "wasm32")]
5105        web_sys::console::warn_1(
5106            &format!(
5107                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5108                &sketch_block_ref
5109            )
5110            .into(),
5111        );
5112        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5113    };
5114    let mut found = false;
5115    for item in ast.body.iter_mut() {
5116        match item {
5117            ast::BodyItem::ImportStatement(_) => {}
5118            ast::BodyItem::ExpressionStatement(node) => {
5119                // Check the statement.
5120                if let Some(node_path) = &node.node_path
5121                    && node_path == target_node_path
5122                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5123                {
5124                    sketch_block.is_being_edited = true;
5125                    found = true;
5126                    break;
5127                }
5128                // Check the expression.
5129                if let Some(node_path) = node.expression.node_path()
5130                    && node_path == target_node_path
5131                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5132                {
5133                    sketch_block.is_being_edited = true;
5134                    found = true;
5135                    break;
5136                }
5137            }
5138            ast::BodyItem::VariableDeclaration(node) => {
5139                if let Some(node_path) = node.declaration.init.node_path()
5140                    && node_path == target_node_path
5141                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5142                {
5143                    sketch_block.is_being_edited = true;
5144                    found = true;
5145                    break;
5146                }
5147            }
5148            ast::BodyItem::TypeDeclaration(_) => {}
5149            ast::BodyItem::ReturnStatement(node) => {
5150                if let Some(node_path) = node.argument.node_path()
5151                    && node_path == target_node_path
5152                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5153                {
5154                    sketch_block.is_being_edited = true;
5155                    found = true;
5156                    break;
5157                }
5158            }
5159        }
5160    }
5161    if !found {
5162        return Err(KclError::refactor(format!(
5163            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5164        )));
5165    }
5166
5167    Ok(())
5168}
5169
5170fn sketch_on_ast_expr(
5171    ast: &mut ast::Node<ast::Program>,
5172    scene_graph: &SceneGraph,
5173    on: &Plane,
5174) -> Result<ast::Expr, KclError> {
5175    match on {
5176        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5177        Plane::Object(object_id) => {
5178            let on_object = scene_graph
5179                .objects
5180                .get(object_id.0)
5181                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5182            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5183                return Ok(face_expr);
5184            }
5185            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5186        }
5187    }
5188}
5189
5190fn sketch_face_of_scene_object_ast_expr(
5191    ast: &mut ast::Node<ast::Program>,
5192    on_object: &crate::front::Object,
5193) -> Result<Option<ast::Expr>, KclError> {
5194    let SourceRef::BackTrace { ranges } = &on_object.source else {
5195        return Ok(None);
5196    };
5197
5198    match &on_object.kind {
5199        ObjectKind::Wall(_) => {
5200            let (solid_range, sweep_range, segment_range) = match ranges.as_slice() {
5201                [sweep_range, segment_range] => (sweep_range, sweep_range, segment_range),
5202                [solid_range, sweep_range, segment_range] => (solid_range, sweep_range, segment_range),
5203                _ => {
5204                    return Err(KclError::refactor(format!(
5205                        "Expected wall source metadata to have 2 or 3 ranges, got {}; artifact_id={:?}",
5206                        ranges.len(),
5207                        on_object.artifact_id
5208                    )));
5209                }
5210            };
5211            let solid_ref = get_or_insert_ast_reference(
5212                ast,
5213                &SourceRef::Simple {
5214                    range: solid_range.0,
5215                    node_path: solid_range.1.clone(),
5216                },
5217                "solid",
5218                None,
5219            )?;
5220            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5221                return Err(KclError::refactor(format!(
5222                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5223                    on_object.artifact_id
5224                )));
5225            };
5226            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5227            let sweep_ref = get_or_insert_ast_reference(
5228                ast,
5229                &SourceRef::Simple {
5230                    range: sweep_range.0,
5231                    node_path: sweep_range.1.clone(),
5232                },
5233                "solid",
5234                None,
5235            )?;
5236            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5237                return Err(KclError::refactor(format!(
5238                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5239                    on_object.artifact_id
5240                )));
5241            };
5242            let sweep_name = sweep_name_expr.name.name.clone();
5243            let segment_ref = get_or_insert_ast_reference(
5244                ast,
5245                &SourceRef::Simple {
5246                    range: segment_range.0,
5247                    node_path: segment_range.1.clone(),
5248                },
5249                LINE_VARIABLE,
5250                None,
5251            )?;
5252
5253            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name) {
5254                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5255                    return Err(KclError::refactor(format!(
5256                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5257                        on_object.artifact_id
5258                    )));
5259                };
5260                create_member_expression(
5261                    create_member_expression(ast_name_expr(region_name), "tags"),
5262                    &segment_name_expr.name.name,
5263                )
5264            } else {
5265                segment_ref
5266            };
5267
5268            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5269        }
5270        ObjectKind::Cap(cap) => {
5271            let solid_range = match ranges.as_slice() {
5272                [solid_range] | [solid_range, _] => solid_range,
5273                _ => {
5274                    return Err(KclError::refactor(format!(
5275                        "Expected cap source metadata to have 1 or 2 ranges, got {}; artifact_id={:?}",
5276                        ranges.len(),
5277                        on_object.artifact_id
5278                    )));
5279                }
5280            };
5281            let solid_ref = get_or_insert_ast_reference(
5282                ast,
5283                &SourceRef::Simple {
5284                    range: solid_range.0,
5285                    node_path: solid_range.1.clone(),
5286                },
5287                "solid",
5288                None,
5289            )?;
5290            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5291                return Err(KclError::refactor(format!(
5292                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5293                    on_object.artifact_id
5294                )));
5295            };
5296            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5297            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5298            let face_expr = match cap.kind {
5299                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5300                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5301            };
5302
5303            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5304        }
5305        _ => Ok(None),
5306    }
5307}
5308
5309fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5310    let mut current_id = source_id;
5311    let mut current_composite = None;
5312    let mut visited = HashSet::new();
5313
5314    while visited.insert(current_id) {
5315        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5316
5317        let Some(composite_id) = next_composite_id else {
5318            break;
5319        };
5320        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5321            break;
5322        };
5323
5324        current_id = composite.id;
5325        current_composite = Some(composite);
5326
5327        if !composite.consumed {
5328            break;
5329        }
5330    }
5331
5332    current_composite.map(|composite| &composite.code_ref)
5333}
5334
5335fn downstream_composite_id_for_solid_source(
5336    artifact_graph: &ArtifactGraph,
5337    source_id: ArtifactId,
5338) -> Option<ArtifactId> {
5339    // Source is a path, find its solid.
5340    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5341        && let Some(composite_id) = path.composite_solid_id
5342        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5343        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5344    {
5345        return Some(composite_id);
5346    }
5347
5348    // Source is a sweep, find its path -> then find the solid
5349    for artifact in artifact_graph.values() {
5350        if let Artifact::Path(path) = artifact
5351            && path.sweep_id == Some(source_id)
5352            && let Some(composite_id) = path.composite_solid_id
5353            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5354            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5355        {
5356            return Some(composite_id);
5357        }
5358    }
5359
5360    // Source is a solid, find its downstream solid.
5361    artifact_graph.values().find_map(|artifact| {
5362        let Artifact::CompositeSolid(composite) = artifact else {
5363            return None;
5364        };
5365        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5366    })
5367}
5368
5369fn composite_contains_path_input(
5370    solid_ids: &[ArtifactId],
5371    tool_ids: &[ArtifactId],
5372    path_id: ArtifactId,
5373    solid2d_id: Option<ArtifactId>,
5374) -> bool {
5375    composite_contains_input(solid_ids, tool_ids, path_id)
5376        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5377}
5378
5379fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5380    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5381}
5382
5383fn code_ref_source_ref_range(code_ref: &CodeRef) -> (SourceRange, Option<crate::NodePath>) {
5384    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5385    (code_ref.range, node_path)
5386}
5387
5388fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5389    let mut existing_artifact_ids = scene_objects
5390        .iter()
5391        .map(|object| object.artifact_id)
5392        .collect::<HashSet<_>>();
5393
5394    for artifact in artifact_graph.values() {
5395        match artifact {
5396            Artifact::Wall(wall) => {
5397                if existing_artifact_ids.contains(&wall.id) {
5398                    continue;
5399                }
5400
5401                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5402                    Artifact::Segment(segment) => Some(segment),
5403                    _ => None,
5404                }) else {
5405                    continue;
5406                };
5407                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5408                    Artifact::Sweep(sweep) => Some(sweep),
5409                    _ => None,
5410                }) else {
5411                    continue;
5412                };
5413                let source_segment = segment
5414                    .original_seg_id
5415                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5416                    .and_then(|artifact| match artifact {
5417                        Artifact::Segment(segment) => Some(segment),
5418                        _ => None,
5419                    })
5420                    .unwrap_or(segment);
5421                let solid_code_ref =
5422                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5423                let mut ranges = Vec::new();
5424                if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5425                {
5426                    ranges.push(code_ref_source_ref_range(solid_code_ref));
5427                }
5428                ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5429                ranges.push(code_ref_source_ref_range(&source_segment.code_ref));
5430                let id = ObjectId(scene_objects.len());
5431                scene_objects.push(crate::front::Object {
5432                    id,
5433                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5434                    label: Default::default(),
5435                    comments: Default::default(),
5436                    artifact_id: wall.id,
5437                    source: SourceRef::BackTrace { ranges },
5438                });
5439                existing_artifact_ids.insert(wall.id);
5440            }
5441            Artifact::Cap(cap) => {
5442                if existing_artifact_ids.contains(&cap.id) {
5443                    continue;
5444                }
5445
5446                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5447                    Artifact::Sweep(sweep) => Some(sweep),
5448                    _ => None,
5449                }) else {
5450                    continue;
5451                };
5452                let id = ObjectId(scene_objects.len());
5453                let kind = match cap.sub_type {
5454                    CapSubType::Start => crate::frontend::api::CapKind::Start,
5455                    CapSubType::End => crate::frontend::api::CapKind::End,
5456                };
5457                let solid_code_ref =
5458                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5459                let mut ranges = Vec::new();
5460                if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5461                {
5462                    ranges.push(code_ref_source_ref_range(solid_code_ref));
5463                }
5464                ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5465                scene_objects.push(crate::front::Object {
5466                    id,
5467                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5468                    label: Default::default(),
5469                    comments: Default::default(),
5470                    artifact_id: cap.id,
5471                    source: SourceRef::BackTrace { ranges },
5472                });
5473                existing_artifact_ids.insert(cap.id);
5474            }
5475            _ => {}
5476        }
5477    }
5478}
5479
5480fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5481    use crate::engine::PlaneName;
5482
5483    match name {
5484        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5485        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5486        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5487        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5488        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5489        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5490    }
5491}
5492
5493fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5494    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5495        ast::UnaryOperator::Neg,
5496        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5497    )))
5498}
5499
5500fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5501    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5502        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5503        unlabeled: Some(solid_expr),
5504        arguments: vec![ast::LabeledArg {
5505            label: Some(ast::Identifier::new("face")),
5506            arg: face_expr,
5507        }],
5508        digest: None,
5509        non_code_meta: Default::default(),
5510    })))
5511}
5512
5513fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5514    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5515        return None;
5516    };
5517    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5518        return None;
5519    };
5520    if !matches!(
5521        sweep_call.callee.name.name.as_str(),
5522        "extrude" | "revolve" | "sweep" | "loft"
5523    ) {
5524        return None;
5525    }
5526    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5527        return None;
5528    };
5529    let candidate = region_name_expr.name.name.clone();
5530    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5531        return None;
5532    };
5533    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5534        return None;
5535    };
5536    if region_call.callee.name.name != "region" {
5537        return None;
5538    }
5539    Some(candidate)
5540}
5541
5542/// Return the AST expression referencing the variable at the given source ref.
5543/// If no such variable exists, insert a new variable declaration with the given
5544/// prefix.
5545///
5546/// This may return a complex expression referencing properties of the variable
5547/// (e.g., `line1.start`).
5548fn get_or_insert_ast_reference(
5549    ast: &mut ast::Node<ast::Program>,
5550    source_ref: &SourceRef,
5551    prefix: &str,
5552    property: Option<&str>,
5553) -> Result<ast::Expr, KclError> {
5554    let command = AstMutateCommand::AddVariableDeclaration {
5555        prefix: prefix.to_owned(),
5556    };
5557    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5558        Ok((_, ret)) => ret,
5559        Err(err) => {
5560            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5561                AstMutateCommandReturn::Name(var_name)
5562            } else {
5563                return Err(err);
5564            }
5565        }
5566    };
5567    let AstMutateCommandReturn::Name(var_name) = ret else {
5568        return Err(KclError::refactor(
5569            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5570        ));
5571    };
5572    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5573    let Some(property) = property else {
5574        // No property; just return the variable name.
5575        return Ok(var_expr);
5576    };
5577
5578    Ok(create_member_expression(var_expr, property))
5579}
5580
5581fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5582    let source_range = match source_ref {
5583        SourceRef::Simple { range, .. } => *range,
5584        SourceRef::BackTrace { ranges } => {
5585            let [range] = ranges.as_slice() else {
5586                return None;
5587            };
5588            range.0
5589        }
5590    };
5591    ast.body.iter().find_map(|item| {
5592        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5593            return None;
5594        };
5595        let init_range = SourceRange::from(&var_decl.declaration.init);
5596        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5597            && init_range.start() <= source_range.start()
5598            && source_range.end() <= init_range.end();
5599        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5600            && init_range != source_range
5601            && source_is_inside_init
5602        {
5603            return None;
5604        }
5605        source_is_inside_init.then(|| var_decl.name().to_owned())
5606    })
5607}
5608
5609fn mutate_ast_node_by_source_ref(
5610    ast: &mut ast::Node<ast::Program>,
5611    source_ref: &SourceRef,
5612    command: AstMutateCommand,
5613) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5614    let (source_range, node_path) = match source_ref {
5615        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5616        SourceRef::BackTrace { ranges } => {
5617            let [range] = ranges.as_slice() else {
5618                return Err(KclError::refactor(format!(
5619                    "Expected single source ref, got {}; ranges={ranges:#?}",
5620                    ranges.len(),
5621                )));
5622            };
5623            (range.0, range.1.clone())
5624        }
5625    };
5626    let mut context = AstMutateContext {
5627        source_range,
5628        node_path,
5629        command,
5630        defined_names_stack: Default::default(),
5631    };
5632    let control = dfs_mut(ast, &mut context);
5633    match control {
5634        ControlFlow::Continue(_) => Err(KclError::refactor(
5635            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5636        )),
5637        ControlFlow::Break(break_value) => break_value,
5638    }
5639}
5640
5641#[derive(Debug)]
5642struct AstMutateContext {
5643    source_range: SourceRange,
5644    node_path: Option<ast::NodePath>,
5645    command: AstMutateCommand,
5646    defined_names_stack: Vec<HashSet<String>>,
5647}
5648
5649#[derive(Debug)]
5650#[allow(clippy::large_enum_variant)]
5651enum AstMutateCommand {
5652    /// Add an expression statement to the sketch block.
5653    AddSketchBlockExprStmt {
5654        expr: ast::Expr,
5655    },
5656    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5657    AddSketchBlockVarDecl {
5658        prefix: String,
5659        expr: ast::Expr,
5660    },
5661    AddVariableDeclaration {
5662        prefix: String,
5663    },
5664    EditPoint {
5665        at: ast::Expr,
5666    },
5667    EditLine {
5668        start: ast::Expr,
5669        end: ast::Expr,
5670        construction: Option<bool>,
5671    },
5672    EditArc {
5673        start: ast::Expr,
5674        end: ast::Expr,
5675        center: ast::Expr,
5676        construction: Option<bool>,
5677    },
5678    EditCircle {
5679        start: ast::Expr,
5680        center: ast::Expr,
5681        construction: Option<bool>,
5682    },
5683    EditControlPointSpline {
5684        points: ast::Expr,
5685        construction: Option<bool>,
5686    },
5687    EditConstraintValue {
5688        value: ast::BinaryPart,
5689    },
5690    EditDistanceConstraintLabelPosition {
5691        label_position: ast::Expr,
5692    },
5693    EditCallUnlabeled {
5694        arg: ast::Expr,
5695    },
5696    EditVarInitialValue {
5697        value: Number,
5698    },
5699    DeleteNode,
5700}
5701
5702impl AstMutateCommand {
5703    fn needs_defined_names_stack(&self) -> bool {
5704        matches!(
5705            self,
5706            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5707        )
5708    }
5709}
5710
5711#[derive(Debug)]
5712enum AstMutateCommandReturn {
5713    None,
5714    Name(String),
5715}
5716
5717#[derive(Debug, Clone)]
5718struct AstNodeRef {
5719    range: SourceRange,
5720    node_path: Option<ast::NodePath>,
5721}
5722
5723impl<T> From<&ast::Node<T>> for AstNodeRef {
5724    fn from(value: &ast::Node<T>) -> Self {
5725        AstNodeRef {
5726            range: value.into(),
5727            node_path: value.node_path.clone(),
5728        }
5729    }
5730}
5731
5732impl From<&ast::BodyItem> for AstNodeRef {
5733    fn from(value: &ast::BodyItem) -> Self {
5734        match value {
5735            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5736                range: node.into(),
5737                node_path: node.node_path.clone(),
5738            },
5739            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5740                range: node.into(),
5741                node_path: node.node_path.clone(),
5742            },
5743            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5744                range: node.into(),
5745                node_path: node.node_path.clone(),
5746            },
5747            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5748                range: node.into(),
5749                node_path: node.node_path.clone(),
5750            },
5751            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5752                range: node.into(),
5753                node_path: node.node_path.clone(),
5754            },
5755        }
5756    }
5757}
5758
5759impl From<&ast::Expr> for AstNodeRef {
5760    fn from(value: &ast::Expr) -> Self {
5761        AstNodeRef {
5762            range: SourceRange::from(value),
5763            node_path: value.node_path().cloned(),
5764        }
5765    }
5766}
5767
5768impl From<&AstMutateContext> for AstNodeRef {
5769    fn from(value: &AstMutateContext) -> Self {
5770        AstNodeRef {
5771            range: value.source_range,
5772            node_path: value.node_path.clone(),
5773        }
5774    }
5775}
5776
5777impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5778    type Error = crate::walk::AstNodeError;
5779
5780    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5781        Ok(AstNodeRef {
5782            range: SourceRange::try_from(value)?,
5783            node_path: value.try_into()?,
5784        })
5785    }
5786}
5787
5788impl From<AstNodeRef> for SourceRange {
5789    fn from(value: AstNodeRef) -> Self {
5790        value.range
5791    }
5792}
5793
5794impl Visitor for AstMutateContext {
5795    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5796    type Continue = ();
5797
5798    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5799        filter_and_process(self, node)
5800    }
5801
5802    fn finish(&mut self, node: NodeMut<'_>) {
5803        match &node {
5804            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5805                self.defined_names_stack.pop();
5806            }
5807            _ => {}
5808        }
5809    }
5810}
5811
5812fn filter_and_process(
5813    ctx: &mut AstMutateContext,
5814    node: NodeMut,
5815) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5816    let Ok(node_range) = SourceRange::try_from(&node) else {
5817        // Nodes that can't be converted to a range aren't interesting.
5818        return TraversalReturn::new_continue(());
5819    };
5820    // If we're adding a variable declaration, we need to look at variable
5821    // declaration expressions to see if it already has a variable, before
5822    // continuing. The variable declaration's source range won't match the
5823    // target; its init expression will.
5824    if let NodeMut::VariableDeclaration(var_decl) = &node {
5825        let expr_range = SourceRange::from(&var_decl.declaration.init);
5826        let expr_node_path = var_decl.declaration.init.node_path();
5827        if source_ref_matches(ctx, expr_range, expr_node_path) {
5828            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5829                // We found the variable declaration expression. It doesn't need
5830                // to be added.
5831                return TraversalReturn::new_break(Ok((
5832                    AstNodeRef::from(&**var_decl),
5833                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5834                )));
5835            }
5836            if let AstMutateCommand::DeleteNode = &ctx.command {
5837                // We found the variable declaration. Delete the variable along
5838                // with the segment.
5839                return TraversalReturn {
5840                    mutate_body_item: MutateBodyItem::Delete,
5841                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5842                };
5843            }
5844        }
5845    }
5846    // Similar thing with expression statement. We need to look at the
5847    // expression inside it.
5848    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5849        let expr_range = SourceRange::from(&expr_stmt.expression);
5850        let expr_node_path = expr_stmt.expression.node_path();
5851        if source_ref_matches(ctx, expr_range, expr_node_path) {
5852            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5853                // We found the node wrapped in an expression statement. Process
5854                // the statement.
5855                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5856                    return TraversalReturn::new_continue(());
5857                };
5858                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5859            }
5860            if let AstMutateCommand::DeleteNode = &ctx.command {
5861                // We found the node wrapped in an expression statement. Delete
5862                // the whole statement.
5863                return TraversalReturn {
5864                    mutate_body_item: MutateBodyItem::Delete,
5865                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5866                };
5867            }
5868        }
5869    }
5870
5871    if ctx.command.needs_defined_names_stack() {
5872        if let NodeMut::Program(program) = &node {
5873            ctx.defined_names_stack.push(find_defined_names(*program));
5874        } else if let NodeMut::SketchBlock(block) = &node {
5875            ctx.defined_names_stack.push(find_defined_names(&block.body));
5876        }
5877    }
5878
5879    // Make sure the node matches the source ref.
5880    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5881    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5882        return TraversalReturn::new_continue(());
5883    }
5884    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5885        return TraversalReturn::new_continue(());
5886    };
5887    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5888}
5889
5890fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5891    match &ctx.node_path {
5892        Some(target) => Some(target) == node_path,
5893        None => node_range == ctx.source_range,
5894    }
5895}
5896
5897fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5898    match &ctx.command {
5899        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5900            if let NodeMut::SketchBlock(sketch_block) = node {
5901                sketch_block
5902                    .body
5903                    .items
5904                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5905                        inner: ast::ExpressionStatement {
5906                            expression: expr.clone(),
5907                            digest: None,
5908                        },
5909                        start: Default::default(),
5910                        end: Default::default(),
5911                        module_id: Default::default(),
5912                        node_path: None,
5913                        outer_attrs: Default::default(),
5914                        pre_comments: Default::default(),
5915                        comment_start: Default::default(),
5916                    }));
5917                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5918            }
5919        }
5920        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5921            if let NodeMut::SketchBlock(sketch_block) = node {
5922                let empty_defined_names = HashSet::new();
5923                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5924                let Ok(name) = next_free_name(prefix, defined_names) else {
5925                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5926                };
5927                sketch_block
5928                    .body
5929                    .items
5930                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5931                        ast::VariableDeclaration::new(
5932                            ast::VariableDeclarator::new(&name, expr.clone()),
5933                            ast::ItemVisibility::Default,
5934                            ast::VariableKind::Const,
5935                        ),
5936                    ))));
5937                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5938            }
5939        }
5940        AstMutateCommand::AddVariableDeclaration { prefix } => {
5941            if let NodeMut::VariableDeclaration(inner) = node {
5942                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5943            }
5944            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5945                let empty_defined_names = HashSet::new();
5946                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5947                let Ok(name) = next_free_name(prefix, defined_names) else {
5948                    // TODO: Return an error instead?
5949                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5950                };
5951                let mutate_node =
5952                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5953                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5954                        ast::ItemVisibility::Default,
5955                        ast::VariableKind::Const,
5956                    ))));
5957                return TraversalReturn {
5958                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5959                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5960                };
5961            }
5962        }
5963        AstMutateCommand::EditPoint { at } => {
5964            if let NodeMut::CallExpressionKw(call) = node {
5965                if call.callee.name.name != POINT_FN {
5966                    return TraversalReturn::new_continue(());
5967                }
5968                // Update the arguments.
5969                for labeled_arg in &mut call.arguments {
5970                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5971                        labeled_arg.arg = at.clone();
5972                    }
5973                }
5974                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5975            }
5976        }
5977        AstMutateCommand::EditLine {
5978            start,
5979            end,
5980            construction,
5981        } => {
5982            if let NodeMut::CallExpressionKw(call) = node {
5983                if call.callee.name.name != LINE_FN {
5984                    return TraversalReturn::new_continue(());
5985                }
5986                // Update the arguments.
5987                for labeled_arg in &mut call.arguments {
5988                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5989                        labeled_arg.arg = start.clone();
5990                    }
5991                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5992                        labeled_arg.arg = end.clone();
5993                    }
5994                }
5995                // Handle construction kwarg
5996                if let Some(construction_value) = construction {
5997                    let construction_exists = call
5998                        .arguments
5999                        .iter()
6000                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6001                    if *construction_value {
6002                        // Add or update construction=true
6003                        if construction_exists {
6004                            // Update existing construction kwarg
6005                            for labeled_arg in &mut call.arguments {
6006                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6007                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6008                                        value: ast::LiteralValue::Bool(true),
6009                                        raw: "true".to_string(),
6010                                        digest: None,
6011                                    })));
6012                                }
6013                            }
6014                        } else {
6015                            // Add new construction kwarg
6016                            call.arguments.push(ast::LabeledArg {
6017                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6018                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6019                                    value: ast::LiteralValue::Bool(true),
6020                                    raw: "true".to_string(),
6021                                    digest: None,
6022                                }))),
6023                            });
6024                        }
6025                    } else {
6026                        // Remove construction kwarg if it exists
6027                        call.arguments
6028                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6029                    }
6030                }
6031                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6032            }
6033        }
6034        AstMutateCommand::EditArc {
6035            start,
6036            end,
6037            center,
6038            construction,
6039        } => {
6040            if let NodeMut::CallExpressionKw(call) = node {
6041                if call.callee.name.name != ARC_FN {
6042                    return TraversalReturn::new_continue(());
6043                }
6044                // Update the arguments.
6045                for labeled_arg in &mut call.arguments {
6046                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6047                        labeled_arg.arg = start.clone();
6048                    }
6049                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6050                        labeled_arg.arg = end.clone();
6051                    }
6052                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6053                        labeled_arg.arg = center.clone();
6054                    }
6055                }
6056                // Handle construction kwarg
6057                if let Some(construction_value) = construction {
6058                    let construction_exists = call
6059                        .arguments
6060                        .iter()
6061                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6062                    if *construction_value {
6063                        // Add or update construction=true
6064                        if construction_exists {
6065                            // Update existing construction kwarg
6066                            for labeled_arg in &mut call.arguments {
6067                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6068                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6069                                        value: ast::LiteralValue::Bool(true),
6070                                        raw: "true".to_string(),
6071                                        digest: None,
6072                                    })));
6073                                }
6074                            }
6075                        } else {
6076                            // Add new construction kwarg
6077                            call.arguments.push(ast::LabeledArg {
6078                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6079                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6080                                    value: ast::LiteralValue::Bool(true),
6081                                    raw: "true".to_string(),
6082                                    digest: None,
6083                                }))),
6084                            });
6085                        }
6086                    } else {
6087                        // Remove construction kwarg if it exists
6088                        call.arguments
6089                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6090                    }
6091                }
6092                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6093            }
6094        }
6095        AstMutateCommand::EditCircle {
6096            start,
6097            center,
6098            construction,
6099        } => {
6100            if let NodeMut::CallExpressionKw(call) = node {
6101                if call.callee.name.name != CIRCLE_FN {
6102                    return TraversalReturn::new_continue(());
6103                }
6104                // Update the arguments.
6105                for labeled_arg in &mut call.arguments {
6106                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6107                        labeled_arg.arg = start.clone();
6108                    }
6109                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6110                        labeled_arg.arg = center.clone();
6111                    }
6112                }
6113                // Handle construction kwarg
6114                if let Some(construction_value) = construction {
6115                    let construction_exists = call
6116                        .arguments
6117                        .iter()
6118                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6119                    if *construction_value {
6120                        if construction_exists {
6121                            // Update existing construction kwarg
6122                            for labeled_arg in &mut call.arguments {
6123                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6124                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6125                                        value: ast::LiteralValue::Bool(true),
6126                                        raw: "true".to_string(),
6127                                        digest: None,
6128                                    })));
6129                                }
6130                            }
6131                        } else {
6132                            // Add new construction kwarg
6133                            call.arguments.push(ast::LabeledArg {
6134                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6135                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6136                                    value: ast::LiteralValue::Bool(true),
6137                                    raw: "true".to_string(),
6138                                    digest: None,
6139                                }))),
6140                            });
6141                        }
6142                    } else {
6143                        // Remove construction kwarg if it exists
6144                        call.arguments
6145                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6146                    }
6147                }
6148                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6149            }
6150        }
6151        AstMutateCommand::EditControlPointSpline { points, construction } => {
6152            if let NodeMut::CallExpressionKw(call) = node {
6153                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6154                    return TraversalReturn::new_continue(());
6155                }
6156                for labeled_arg in &mut call.arguments {
6157                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6158                    {
6159                        labeled_arg.arg = points.clone();
6160                    }
6161                }
6162                // Handle construction kwarg
6163                if let Some(construction_value) = construction {
6164                    let construction_exists = call
6165                        .arguments
6166                        .iter()
6167                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6168                    if *construction_value {
6169                        if construction_exists {
6170                            for labeled_arg in &mut call.arguments {
6171                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6172                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6173                                        value: ast::LiteralValue::Bool(true),
6174                                        raw: "true".to_string(),
6175                                        digest: None,
6176                                    })));
6177                                }
6178                            }
6179                        } else {
6180                            call.arguments.push(ast::LabeledArg {
6181                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6182                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6183                                    value: ast::LiteralValue::Bool(true),
6184                                    raw: "true".to_string(),
6185                                    digest: None,
6186                                }))),
6187                            });
6188                        }
6189                    } else {
6190                        call.arguments
6191                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6192                    }
6193                }
6194                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6195            }
6196        }
6197        AstMutateCommand::EditConstraintValue { value } => {
6198            if let NodeMut::BinaryExpression(binary_expr) = node {
6199                let left_is_constraint = matches!(
6200                    &binary_expr.left,
6201                    ast::BinaryPart::CallExpressionKw(call)
6202                        if matches!(
6203                            call.callee.name.name.as_str(),
6204                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6205                        )
6206                );
6207                if left_is_constraint {
6208                    binary_expr.right = value.clone();
6209                } else {
6210                    binary_expr.left = value.clone();
6211                }
6212
6213                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6214            }
6215        }
6216        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6217            if let NodeMut::BinaryExpression(binary_expr) = node {
6218                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6219                    return TraversalReturn::new_continue(());
6220                };
6221                if !matches!(
6222                    call.callee.name.name.as_str(),
6223                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6224                ) {
6225                    return TraversalReturn::new_continue(());
6226                }
6227
6228                if let Some(label_arg) = call
6229                    .arguments
6230                    .iter_mut()
6231                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6232                {
6233                    label_arg.arg = label_position.clone();
6234                } else {
6235                    call.arguments.push(ast::LabeledArg {
6236                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6237                        arg: label_position.clone(),
6238                    });
6239                }
6240
6241                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6242            }
6243        }
6244        AstMutateCommand::EditCallUnlabeled { arg } => {
6245            if let NodeMut::CallExpressionKw(call) = node {
6246                call.unlabeled = Some(arg.clone());
6247                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6248            }
6249        }
6250        AstMutateCommand::EditVarInitialValue { value } => {
6251            // We target the SketchVar itself (matched by NodePath) rather than
6252            // the inner NumericLiteral so we can also write back into vars that
6253            // were declared without an initial value (e.g. bare `var`).
6254            if let NodeMut::SketchVar(sketch_var) = node {
6255                let Ok(literal) = to_source_number(*value) else {
6256                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6257                        "Could not convert number to AST literal: {:?}",
6258                        *value
6259                    ))));
6260                };
6261                sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6262                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6263            }
6264        }
6265        AstMutateCommand::DeleteNode => {
6266            return TraversalReturn {
6267                mutate_body_item: MutateBodyItem::Delete,
6268                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6269            };
6270        }
6271    }
6272    TraversalReturn::new_continue(())
6273}
6274
6275struct FindSketchBlockSourceRange {
6276    /// The source range of the sketch block before mutation.
6277    target_before_mutation: SourceRange,
6278    /// The source range of the sketch block's last body item after mutation. We
6279    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6280    /// shared reference.
6281    found: Cell<Option<AstNodeRef>>,
6282}
6283
6284impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6285    type Error = crate::front::Error;
6286
6287    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6288        let Ok(node_range) = SourceRange::try_from(&node) else {
6289            return Ok(true);
6290        };
6291
6292        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6293            if node_range.module_id() == self.target_before_mutation.module_id()
6294                && node_range.start() == self.target_before_mutation.start()
6295                // End shouldn't match since we added something.
6296                && node_range.end() >= self.target_before_mutation.end()
6297            {
6298                self.found.set(sketch_block.body.items.last().map(|item| match item {
6299                    // For declarations like `circle1 = circle(...)`, use
6300                    // the init expression range so lookup in source_range_to_object
6301                    // matches the segment source range.
6302                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6303                    _ => AstNodeRef::from(item),
6304                }));
6305                return Ok(false);
6306            } else {
6307                // We found a different sketch block. No need to descend into
6308                // its children since sketch blocks cannot be nested.
6309                return Ok(true);
6310            }
6311        }
6312
6313        for child in node.children().iter() {
6314            if !child.visit(*self)? {
6315                return Ok(false);
6316            }
6317        }
6318
6319        Ok(true)
6320    }
6321}
6322
6323struct FindSketchBlockByNodePath {
6324    /// The Node Path of the sketch block before mutation.
6325    target_node_path: ast::NodePath,
6326    /// The ref of the sketch block's last body item after mutation. We need to
6327    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6328    /// reference.
6329    found: Cell<Option<AstNodeRef>>,
6330}
6331
6332impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6333    type Error = crate::front::Error;
6334
6335    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6336        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6337            return Ok(true);
6338        };
6339
6340        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6341            if let Some(node_path) = node_path
6342                && node_path == self.target_node_path
6343            {
6344                self.found.set(sketch_block.body.items.last().map(|item| match item {
6345                    // For declarations like `circle1 = circle(...)`, use
6346                    // the init expression range so lookup in source_range_to_object
6347                    // matches the segment source range.
6348                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6349                    _ => AstNodeRef::from(item),
6350                }));
6351
6352                return Ok(false);
6353            } else {
6354                // We found a different sketch block. No need to descend into
6355                // its children since sketch blocks cannot be nested.
6356                return Ok(true);
6357            }
6358        }
6359
6360        for child in node.children().iter() {
6361            if !child.visit(*self)? {
6362                return Ok(false);
6363            }
6364        }
6365
6366        Ok(true)
6367    }
6368}
6369
6370/// After adding an item to a sketch block, find the sketch block, and get the
6371/// source range of the added item. We assume that the added item is the last
6372/// item in the sketch block and that the sketch block's source range has grown,
6373/// but not moved from its starting offset.
6374///
6375/// TODO: Do we need to format *before* mutation in case formatting moves the
6376/// sketch block forward?
6377fn find_sketch_block_added_item(
6378    ast: &ast::Node<ast::Program>,
6379    sketch_block_before_mutation: &AstNodeRef,
6380) -> Result<AstNodeRef, KclError> {
6381    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6382        let find = FindSketchBlockByNodePath {
6383            target_node_path: node_path.clone(),
6384            found: Cell::new(None),
6385        };
6386        let node = crate::walk::Node::from(ast);
6387        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6388        find.found.into_inner().ok_or_else(|| {
6389            KclError::refactor(format!(
6390                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6391            ))
6392        })
6393    } else {
6394        // No NodePath. Fall back to legacy source range.
6395        let find = FindSketchBlockSourceRange {
6396            target_before_mutation: sketch_block_before_mutation.range,
6397            found: Cell::new(None),
6398        };
6399        let node = crate::walk::Node::from(ast);
6400        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6401        find.found.into_inner().ok_or_else(|| KclError::refactor(
6402            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?"),
6403        ))
6404    }
6405}
6406
6407fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6408    // TODO: Don't duplicate this from lib.rs Program.
6409    ast.recast_top(&Default::default(), 0)
6410}
6411
6412struct FindNumericLiteral {
6413    target: SourceRange,
6414    found: Cell<Option<ast::NumericLiteral>>,
6415}
6416
6417impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6418    type Error = crate::front::Error;
6419
6420    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6421        let Ok(node_range) = SourceRange::try_from(&node) else {
6422            return Ok(true);
6423        };
6424
6425        if node_range == self.target
6426            && let crate::walk::Node::NumericLiteral(literal) = node
6427        {
6428            self.found.set(Some(literal.inner.clone()));
6429            return Ok(false);
6430        }
6431
6432        for child in node.children().iter() {
6433            if !child.visit(*self)? {
6434                return Ok(false);
6435            }
6436        }
6437
6438        Ok(true)
6439    }
6440}
6441
6442fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6443    let find = FindNumericLiteral {
6444        target,
6445        found: Cell::new(None),
6446    };
6447    let node = crate::walk::Node::from(ast);
6448    node.visit(&find).ok()?;
6449    find.found.into_inner()
6450}
6451
6452struct FindSketchVarInitialByNodePath<'a> {
6453    target: &'a ast::NodePath,
6454    sketch_var_found: Cell<bool>,
6455    initial_literal: Cell<Option<ast::NumericLiteral>>,
6456}
6457
6458impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6459    type Error = crate::front::Error;
6460
6461    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6462        if let crate::walk::Node::SketchVar(sketch_var) = node
6463            && sketch_var.node_path.as_ref() == Some(self.target)
6464        {
6465            self.sketch_var_found.set(true);
6466            if let Some(initial) = &sketch_var.initial {
6467                self.initial_literal.set(Some(initial.inner.clone()));
6468            }
6469            return Ok(false);
6470        }
6471
6472        for child in node.children().iter() {
6473            if !child.visit(*self)? {
6474                return Ok(false);
6475            }
6476        }
6477
6478        Ok(true)
6479    }
6480}
6481
6482/// Locate the source `var` declaration corresponding to a sketch-var solution.
6483///
6484/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6485/// from "target found." The inner [`Option`] is the initial numeric literal of
6486/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6487///
6488/// When `node_path` is `None` (e.g. for older outcomes that predate the
6489/// node-path propagation), this falls back to source-range matching, which
6490/// can break under whitespace shifts elsewhere in the file.
6491fn numeric_literal_at_node_path(
6492    ast: &ast::Node<ast::Program>,
6493    node_path: Option<&ast::NodePath>,
6494    source_range: SourceRange,
6495) -> Option<Option<ast::NumericLiteral>> {
6496    let Some(node_path) = node_path else {
6497        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";
6498        #[cfg(target_arch = "wasm32")]
6499        web_sys::console::warn_1(&message.into());
6500        #[cfg(not(target_arch = "wasm32"))]
6501        eprintln!("WARNING: {message}");
6502        return numeric_literal_at_source_range(ast, source_range).map(Some);
6503    };
6504    let find = FindSketchVarInitialByNodePath {
6505        target: node_path,
6506        sketch_var_found: Cell::new(false),
6507        initial_literal: Cell::new(None),
6508    };
6509    let node = crate::walk::Node::from(ast);
6510    node.visit(&find).ok()?;
6511    if !find.sketch_var_found.get() {
6512        return None;
6513    }
6514    Some(find.initial_literal.into_inner())
6515}
6516
6517fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6518    match suffix {
6519        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6520        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6521        NumericSuffix::M => Some(UnitLength::Meters),
6522        NumericSuffix::Inch => Some(UnitLength::Inches),
6523        NumericSuffix::Ft => Some(UnitLength::Feet),
6524        NumericSuffix::Yd => Some(UnitLength::Yards),
6525        _ => None,
6526    }
6527}
6528
6529fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6530    match suffix_length_unit(number.units) {
6531        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6532        None => number.value,
6533    }
6534}
6535
6536fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6537    match suffix_length_unit(literal.suffix) {
6538        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6539        None => literal.value,
6540    }
6541}
6542
6543fn var_solution_needs_commit(
6544    current_literal: &ast::NumericLiteral,
6545    solved_value: Number,
6546    default_length_unit: UnitLength,
6547) -> bool {
6548    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6549    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6550
6551    (current - solved).abs() > 1e-9
6552}
6553
6554fn preserve_var_solution_literal_style(
6555    current_literal: &ast::NumericLiteral,
6556    solved_value: Number,
6557    default_length_unit: UnitLength,
6558) -> Number {
6559    if current_literal.suffix == NumericSuffix::None {
6560        return Number {
6561            value: number_value_in_default_length_units(solved_value, default_length_unit),
6562            units: NumericSuffix::None,
6563        };
6564    }
6565
6566    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6567        return solved_value;
6568    };
6569
6570    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6571    Number {
6572        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6573        units: current_literal.suffix,
6574    }
6575}
6576
6577pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6578    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6579        inner: ast::ArrayExpression {
6580            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6581            non_code_meta: Default::default(),
6582            digest: None,
6583        },
6584        start: Default::default(),
6585        end: Default::default(),
6586        module_id: Default::default(),
6587        node_path: None,
6588        outer_attrs: Default::default(),
6589        pre_comments: Default::default(),
6590        comment_start: Default::default(),
6591    })))
6592}
6593
6594pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6595    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6596        ast::ArrayExpression {
6597            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6598            digest: None,
6599            non_code_meta: Default::default(),
6600        },
6601    ))))
6602}
6603
6604fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6605    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6606        ast::ArrayExpression {
6607            elements: vec![
6608                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6609                    point.x,
6610                )?)))),
6611                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6612                    point.y,
6613                )?)))),
6614            ],
6615            non_code_meta: Default::default(),
6616            digest: None,
6617        },
6618    ))))
6619}
6620
6621fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6622    match expr {
6623        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6624            inner: ast::Literal::from(to_source_number(*number)?),
6625            start: Default::default(),
6626            end: Default::default(),
6627            module_id: Default::default(),
6628            node_path: None,
6629            outer_attrs: Default::default(),
6630            pre_comments: Default::default(),
6631            comment_start: Default::default(),
6632        }))),
6633        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6634            inner: ast::SketchVar {
6635                initial: Some(Box::new(ast::Node {
6636                    inner: to_source_number(*number)?,
6637                    start: Default::default(),
6638                    end: Default::default(),
6639                    module_id: Default::default(),
6640                    node_path: None,
6641                    outer_attrs: Default::default(),
6642                    pre_comments: Default::default(),
6643                    comment_start: Default::default(),
6644                })),
6645                digest: None,
6646            },
6647            start: Default::default(),
6648            end: Default::default(),
6649            module_id: Default::default(),
6650            node_path: None,
6651            outer_attrs: Default::default(),
6652            pre_comments: Default::default(),
6653            comment_start: Default::default(),
6654        }))),
6655        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6656    }
6657}
6658
6659fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6660    Ok(ast::NumericLiteral {
6661        value: number.value,
6662        suffix: number.units,
6663        raw: format_number_literal(number.value, number.units, None)?,
6664        digest: None,
6665    })
6666}
6667
6668pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6669    ast::Expr::Name(Box::new(ast_name(name)))
6670}
6671
6672fn ast_name(name: String) -> ast::Node<ast::Name> {
6673    ast::Node {
6674        inner: ast::Name {
6675            name: ast::Node {
6676                inner: ast::Identifier { name, digest: None },
6677                start: Default::default(),
6678                end: Default::default(),
6679                module_id: Default::default(),
6680                node_path: None,
6681                outer_attrs: Default::default(),
6682                pre_comments: Default::default(),
6683                comment_start: Default::default(),
6684            },
6685            path: Vec::new(),
6686            abs_path: false,
6687            digest: None,
6688        },
6689        start: Default::default(),
6690        end: Default::default(),
6691        module_id: Default::default(),
6692        node_path: None,
6693        outer_attrs: Default::default(),
6694        pre_comments: Default::default(),
6695        comment_start: Default::default(),
6696    }
6697}
6698
6699pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6700    ast::Name {
6701        name: ast::Node {
6702            inner: ast::Identifier {
6703                name: name.to_owned(),
6704                digest: None,
6705            },
6706            start: Default::default(),
6707            end: Default::default(),
6708            module_id: Default::default(),
6709            node_path: None,
6710            outer_attrs: Default::default(),
6711            pre_comments: Default::default(),
6712            comment_start: Default::default(),
6713        },
6714        path: Default::default(),
6715        abs_path: false,
6716        digest: None,
6717    }
6718}
6719
6720// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
6721
6722/// Create an AST node for coincident([expr1, expr2, ...])
6723pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6724    let elements = exprs.into_iter().collect::<Vec<_>>();
6725    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6726
6727    // Create array [expr1, expr2, ...]
6728    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6729        elements,
6730        digest: None,
6731        non_code_meta: Default::default(),
6732    })));
6733
6734    // Create coincident([...])
6735    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6736        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6737        unlabeled: Some(array_expr),
6738        arguments: Default::default(),
6739        digest: None,
6740        non_code_meta: Default::default(),
6741    })))
6742}
6743
6744/// Create an AST node for line(start = [...], end = [...])
6745pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6746    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6747        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6748        unlabeled: None,
6749        arguments: vec![
6750            ast::LabeledArg {
6751                label: Some(ast::Identifier::new(LINE_START_PARAM)),
6752                arg: start_ast,
6753            },
6754            ast::LabeledArg {
6755                label: Some(ast::Identifier::new(LINE_END_PARAM)),
6756                arg: end_ast,
6757            },
6758        ],
6759        digest: None,
6760        non_code_meta: Default::default(),
6761    })))
6762}
6763
6764/// Create an AST node for arc(start = [...], end = [...], center = [...])
6765pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6766    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6767        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6768        unlabeled: None,
6769        arguments: vec![
6770            ast::LabeledArg {
6771                label: Some(ast::Identifier::new(ARC_START_PARAM)),
6772                arg: start_ast,
6773            },
6774            ast::LabeledArg {
6775                label: Some(ast::Identifier::new(ARC_END_PARAM)),
6776                arg: end_ast,
6777            },
6778            ast::LabeledArg {
6779                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6780                arg: center_ast,
6781            },
6782        ],
6783        digest: None,
6784        non_code_meta: Default::default(),
6785    })))
6786}
6787
6788/// Create an AST node for circle(start = [...], center = [...])
6789pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6790    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6791        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6792        unlabeled: None,
6793        arguments: vec![
6794            ast::LabeledArg {
6795                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6796                arg: start_ast,
6797            },
6798            ast::LabeledArg {
6799                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6800                arg: center_ast,
6801            },
6802        ],
6803        digest: None,
6804        non_code_meta: Default::default(),
6805    })))
6806}
6807
6808/// Create an AST node for horizontal(line)
6809pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6810    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6811        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6812        unlabeled: Some(line_expr),
6813        arguments: Default::default(),
6814        digest: None,
6815        non_code_meta: Default::default(),
6816    })))
6817}
6818
6819/// Create an AST node for vertical(line)
6820pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6821    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6822        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6823        unlabeled: Some(line_expr),
6824        arguments: Default::default(),
6825        digest: None,
6826        non_code_meta: Default::default(),
6827    })))
6828}
6829
6830/// Create a member expression like object.property (e.g., line1.end)
6831pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6832    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6833        object: object_expr,
6834        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6835            name: ast::Node::no_src(ast::Identifier {
6836                name: property.to_string(),
6837                digest: None,
6838            }),
6839            path: Vec::new(),
6840            abs_path: false,
6841            digest: None,
6842        }))),
6843        computed: false,
6844        digest: None,
6845    })))
6846}
6847
6848pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6849    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6850        object: object_expr,
6851        property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6852            value: index as f64,
6853            suffix: NumericSuffix::None,
6854            raw: index.to_string(),
6855            digest: None,
6856        })))),
6857        computed: true,
6858        digest: None,
6859    })))
6860}
6861
6862/// Create an AST node for `fixed([point, [x, y]])`.
6863fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6864    // Create [x, y] array literal.
6865    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6866        position.x,
6867    )?))));
6868    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6869        position.y,
6870    )?))));
6871    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6872        elements: vec![x_literal, y_literal],
6873        digest: None,
6874        non_code_meta: Default::default(),
6875    })));
6876
6877    // Create [point, [x, y]] outer array.
6878    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6879        elements: vec![point_expr, point_array],
6880        digest: None,
6881        non_code_meta: Default::default(),
6882    })));
6883
6884    // Create fixed([...])
6885    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6886        ast::CallExpressionKw {
6887            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6888            unlabeled: Some(array_expr),
6889            arguments: Default::default(),
6890            digest: None,
6891            non_code_meta: Default::default(),
6892        },
6893    ))))
6894}
6895
6896/// Create an AST node for equalLength([line1, line2, ...])
6897pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6898    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6899        elements: line_exprs,
6900        digest: None,
6901        non_code_meta: Default::default(),
6902    })));
6903
6904    // Create equalLength([...])
6905    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6906        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6907        unlabeled: Some(array_expr),
6908        arguments: Default::default(),
6909        digest: None,
6910        non_code_meta: Default::default(),
6911    })))
6912}
6913
6914/// Create an AST node for equalRadius([seg1, seg2, ...])
6915pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6916    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6917        elements: segment_exprs,
6918        digest: None,
6919        non_code_meta: Default::default(),
6920    })));
6921
6922    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6923        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6924        unlabeled: Some(array_expr),
6925        arguments: Default::default(),
6926        digest: None,
6927        non_code_meta: Default::default(),
6928    })))
6929}
6930
6931/// Create an AST node for tangent([seg1, seg2])
6932pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6933    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6934        elements: vec![seg1_expr, seg2_expr],
6935        digest: None,
6936        non_code_meta: Default::default(),
6937    })));
6938
6939    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6940        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6941        unlabeled: Some(array_expr),
6942        arguments: Default::default(),
6943        digest: None,
6944        non_code_meta: Default::default(),
6945    })))
6946}
6947
6948/// Create an AST node for symmetric([input1, input2], axis = line)
6949pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6950    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6951        elements: input_exprs,
6952        digest: None,
6953        non_code_meta: Default::default(),
6954    })));
6955    let arguments = vec![ast::LabeledArg {
6956        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6957        arg: axis_expr,
6958    }];
6959
6960    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6961        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6962        unlabeled: Some(array_expr),
6963        arguments,
6964        digest: None,
6965        non_code_meta: Default::default(),
6966    })))
6967}
6968
6969/// Create an AST node for midpoint(segment, point = point)
6970pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6971    let arguments = vec![ast::LabeledArg {
6972        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6973        arg: point_expr,
6974    }];
6975
6976    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6977        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6978        unlabeled: Some(segment_expr),
6979        arguments,
6980        digest: None,
6981        non_code_meta: Default::default(),
6982    })))
6983}
6984
6985#[cfg(test)]
6986mod tests {
6987    use std::sync;
6988
6989    use super::*;
6990    use crate::engine::PlaneName;
6991    use crate::engine::engine_manager::EngineManager;
6992    use crate::execution::cache::SketchModeState;
6993    use crate::execution::cache::clear_mem_cache;
6994    use crate::execution::cache::read_old_memory;
6995    use crate::execution::cache::write_old_memory;
6996    use crate::front::Distance;
6997    use crate::front::Fixed;
6998    use crate::front::FixedPoint;
6999    use crate::front::Midpoint;
7000    use crate::front::Object;
7001    use crate::front::Plane;
7002    use crate::front::Sketch;
7003    use crate::front::Tangent;
7004    use crate::frontend::sketch::Vertical;
7005    use crate::pretty::NumericSuffix;
7006
7007    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7008        for object in &scene_graph.objects {
7009            if let ObjectKind::Sketch(_) = &object.kind {
7010                return Some(object);
7011            }
7012        }
7013        None
7014    }
7015
7016    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7017        for object in &scene_graph.objects {
7018            if let ObjectKind::Face(_) = &object.kind {
7019                return Some(object);
7020            }
7021        }
7022        None
7023    }
7024
7025    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7026        for object in &scene_graph.objects {
7027            if matches!(&object.kind, ObjectKind::Wall(_)) {
7028                return Some(object.id);
7029            }
7030        }
7031        None
7032    }
7033
7034    #[test]
7035    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7036        let source = "\
7037region001 = region(point = [0.1, 0.1], sketch = s)
7038extrude001 = extrude(region001, length = 5)
7039revolve001 = revolve(region001, axis = Y)
7040sweep001 = sweep(region001, path = path001)
7041loft001 = loft(region001)
7042not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7043";
7044
7045        let program = Program::parse(source).unwrap().0.unwrap();
7046
7047        assert_eq!(
7048            region_name_from_sweep_variable(&program.ast, "extrude001"),
7049            Some("region001".to_owned())
7050        );
7051        assert_eq!(
7052            region_name_from_sweep_variable(&program.ast, "revolve001"),
7053            Some("region001".to_owned())
7054        );
7055        assert_eq!(
7056            region_name_from_sweep_variable(&program.ast, "sweep001"),
7057            Some("region001".to_owned())
7058        );
7059        assert_eq!(
7060            region_name_from_sweep_variable(&program.ast, "loft001"),
7061            Some("region001".to_owned())
7062        );
7063        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7064    }
7065
7066    #[track_caller]
7067    fn expect_sketch(object: &Object) -> &Sketch {
7068        if let ObjectKind::Sketch(sketch) = &object.kind {
7069            sketch
7070        } else {
7071            panic!("Object is not a sketch: {:?}", object);
7072        }
7073    }
7074
7075    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7076        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7077        let ObjectKind::Segment {
7078            segment: Segment::Point(point),
7079        } = &point_object.kind
7080        else {
7081            panic!("Object is not a point segment: {point_object:?}");
7082        };
7083        point.position.clone()
7084    }
7085
7086    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7087        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7088        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7089    }
7090
7091    /// Build a millimeter-valued point expression for concise sketch edit test
7092    /// setup.
7093    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7094        Point2d {
7095            x: Expr::Var(Number {
7096                value: x,
7097                units: NumericSuffix::Mm,
7098            }),
7099            y: Expr::Var(Number {
7100                value: y,
7101                units: NumericSuffix::Mm,
7102            }),
7103        }
7104    }
7105
7106    /// Build a millimeter-valued numeric point for comparing solved scene graph
7107    /// positions.
7108    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7109        Point2d {
7110            x: Number {
7111                value: x,
7112                units: NumericSuffix::Mm,
7113            },
7114            y: Number {
7115                value: y,
7116                units: NumericSuffix::Mm,
7117            },
7118        }
7119    }
7120
7121    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7122        LineCtor {
7123            start: Point2d {
7124                x: Expr::Number(Number { value: start_x, units }),
7125                y: Expr::Number(Number { value: start_y, units }),
7126            },
7127            end: Point2d {
7128                x: Expr::Number(Number { value: end_x, units }),
7129                y: Expr::Number(Number { value: end_y, units }),
7130            },
7131            construction: None,
7132        }
7133    }
7134
7135    async fn create_sketch_with_single_line(
7136        frontend: &mut FrontendState,
7137        ctx: &ExecutorContext,
7138        mock_ctx: &ExecutorContext,
7139        version: Version,
7140    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7141        frontend.program = Program::empty();
7142
7143        let sketch_args = SketchCtor {
7144            on: Plane::Default(PlaneName::Xy),
7145        };
7146        let (_src_delta, _scene_delta, sketch_id) = frontend
7147            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7148            .await
7149            .unwrap();
7150
7151        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7152        let (source_delta, scene_graph_delta) = frontend
7153            .add_segment(mock_ctx, version, sketch_id, segment, None)
7154            .await
7155            .unwrap();
7156        let line_id = *scene_graph_delta
7157            .new_objects
7158            .last()
7159            .expect("Expected line object id to be created");
7160
7161        (sketch_id, line_id, source_delta, scene_graph_delta)
7162    }
7163
7164    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7165        frontend.program = program.clone();
7166        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7167        frontend.update_state_after_exec(outcome, true);
7168    }
7169
7170    #[tokio::test(flavor = "multi_thread")]
7171    async fn test_sketch_checkpoint_round_trip_restores_state() {
7172        let mut frontend = FrontendState::new();
7173        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7174        let mock_ctx = ExecutorContext::new_mock(None).await;
7175        let version = Version(0);
7176
7177        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7178            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7179
7180        let expected_source = source_delta.text.clone();
7181        let expected_scene_graph = frontend.scene_graph.clone();
7182        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7183        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7184
7185        let checkpoint_id = frontend
7186            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7187            .await
7188            .unwrap();
7189
7190        let edited_segments = vec![ExistingSegmentCtor {
7191            id: line_id,
7192            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7193        }];
7194        let (edited_source, _edited_scene) = frontend
7195            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7196            .await
7197            .unwrap();
7198        assert_ne!(edited_source.text, expected_source);
7199
7200        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7201
7202        assert_eq!(restored.source_delta.text, expected_source);
7203        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7204        assert!(restored.scene_graph_delta.invalidates_ids);
7205        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7206        assert_eq!(frontend.scene_graph, expected_scene_graph);
7207        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7208
7209        ctx.close().await;
7210    }
7211
7212    #[tokio::test(flavor = "multi_thread")]
7213    async fn test_sketch_checkpoints_prune_oldest_entries() {
7214        let mut frontend = FrontendState::new();
7215        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7216        let mock_ctx = ExecutorContext::new_mock(None).await;
7217        let version = Version(0);
7218
7219        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7220            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7221
7222        let mut checkpoint_ids = Vec::new();
7223        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7224            checkpoint_ids.push(
7225                frontend
7226                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7227                    .await
7228                    .unwrap(),
7229            );
7230        }
7231
7232        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7233        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7234
7235        let oldest_retained = checkpoint_ids[3];
7236        assert_eq!(
7237            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7238            Some(oldest_retained)
7239        );
7240
7241        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7242        assert!(evicted_restore.is_err());
7243        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7244
7245        frontend
7246            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7247            .await
7248            .unwrap();
7249
7250        ctx.close().await;
7251    }
7252
7253    #[tokio::test(flavor = "multi_thread")]
7254    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7255        let mut frontend = FrontendState::new();
7256        let missing_checkpoint = SketchCheckpointId::new(999);
7257
7258        let err = frontend
7259            .restore_sketch_checkpoint(missing_checkpoint)
7260            .await
7261            .expect_err("Expected restore to fail for missing checkpoint");
7262
7263        assert!(err.msg.contains("Sketch checkpoint not found"));
7264    }
7265
7266    #[tokio::test(flavor = "multi_thread")]
7267    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7268        let mut frontend = FrontendState::new();
7269        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7270        let mock_ctx = ExecutorContext::new_mock(None).await;
7271        let version = Version(0);
7272
7273        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7274            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7275
7276        let checkpoint_a = frontend
7277            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7278            .await
7279            .unwrap();
7280        let checkpoint_b = frontend
7281            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7282            .await
7283            .unwrap();
7284        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7285
7286        frontend.clear_sketch_checkpoints();
7287        assert!(frontend.sketch_checkpoints.is_empty());
7288        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7289        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7290
7291        ctx.close().await;
7292    }
7293
7294    #[tokio::test(flavor = "multi_thread")]
7295    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7296        let mut frontend = FrontendState::new();
7297        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7298        let mock_ctx = ExecutorContext::new_mock(None).await;
7299        let version = Version(0);
7300
7301        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7302            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7303        let old_source = source_delta.text.clone();
7304        let old_checkpoint = frontend
7305            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7306            .await
7307            .unwrap();
7308        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7309
7310        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7311            .unwrap()
7312            .0
7313            .unwrap();
7314
7315        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7316        let SetProgramOutcome::Success {
7317            checkpoint_id: Some(new_checkpoint),
7318            ..
7319        } = result
7320        else {
7321            panic!("Expected Success with a fresh checkpoint baseline");
7322        };
7323
7324        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7325
7326        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7327        assert_eq!(old_restore.source_delta.text, old_source);
7328
7329        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7330        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7331
7332        ctx.close().await;
7333    }
7334
7335    #[tokio::test(flavor = "multi_thread")]
7336    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7337        let mut frontend = FrontendState::new();
7338        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7339        let mock_ctx = ExecutorContext::new_mock(None).await;
7340        let version = Version(0);
7341
7342        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7343            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7344        let old_checkpoint = frontend
7345            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7346            .await
7347            .unwrap();
7348        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7349
7350        let failing_program = Program::parse(
7351            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7352        )
7353        .unwrap()
7354        .0
7355        .unwrap();
7356
7357        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7358        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7359        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7360        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7361
7362        ctx.close().await;
7363    }
7364
7365    #[tokio::test(flavor = "multi_thread")]
7366    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7367        let mut frontend = FrontendState::new();
7368        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7369
7370        let program = Program::parse(
7371            "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",
7372        )
7373        .unwrap()
7374        .0
7375        .unwrap();
7376        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7377        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7378            panic!("Expected successful baseline program execution");
7379        };
7380
7381        clear_mem_cache().await;
7382        assert!(read_old_memory().await.is_none());
7383
7384        let checkpoint_without_mock_memory = frontend
7385            .create_sketch_checkpoint((*exec_outcome).clone())
7386            .await
7387            .unwrap();
7388
7389        write_old_memory(SketchModeState::new_for_tests()).await;
7390        assert!(read_old_memory().await.is_some());
7391
7392        let checkpoint_with_mock_memory = frontend
7393            .create_sketch_checkpoint((*exec_outcome).clone())
7394            .await
7395            .unwrap();
7396
7397        clear_mem_cache().await;
7398        assert!(read_old_memory().await.is_none());
7399
7400        frontend
7401            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7402            .await
7403            .unwrap();
7404        assert!(read_old_memory().await.is_some());
7405
7406        frontend
7407            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7408            .await
7409            .unwrap();
7410        assert!(read_old_memory().await.is_none());
7411
7412        ctx.close().await;
7413    }
7414
7415    #[tokio::test(flavor = "multi_thread")]
7416    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7417        let source = "\
7418sketch(on = XY) {
7419  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7420}
7421
7422bad = missing_name
7423";
7424        let program = Program::parse(source).unwrap().0.unwrap();
7425
7426        let mut frontend = FrontendState::new();
7427
7428        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7429        let mock_ctx = ExecutorContext::new_mock(None).await;
7430        let version = Version(0);
7431        let project_id = ProjectId(0);
7432        let file_id = FileId(0);
7433
7434        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7435            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7436        };
7437
7438        let sketch_id = frontend
7439            .scene_graph
7440            .objects
7441            .iter()
7442            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7443            .expect("Expected sketch object from errored hack_set_program");
7444
7445        frontend
7446            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7447            .await
7448            .unwrap();
7449
7450        ctx.close().await;
7451        mock_ctx.close().await;
7452    }
7453
7454    #[tokio::test(flavor = "multi_thread")]
7455    async fn test_new_sketch_add_point_edit_point() {
7456        let program = Program::empty();
7457
7458        let mut frontend = FrontendState::new();
7459        frontend.program = program;
7460
7461        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7462        let mock_ctx = ExecutorContext::new_mock(None).await;
7463        let version = Version(0);
7464
7465        let sketch_args = SketchCtor {
7466            on: Plane::Default(PlaneName::Xy),
7467        };
7468        let (_src_delta, scene_delta, sketch_id) = frontend
7469            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7470            .await
7471            .unwrap();
7472        assert_eq!(sketch_id, ObjectId(1));
7473        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7474        let sketch_object = &scene_delta.new_graph.objects[1];
7475        assert_eq!(sketch_object.id, ObjectId(1));
7476        assert_eq!(
7477            sketch_object.kind,
7478            ObjectKind::Sketch(Sketch {
7479                args: SketchCtor {
7480                    on: Plane::Default(PlaneName::Xy)
7481                },
7482                plane: ObjectId(0),
7483                segments: vec![],
7484                constraints: vec![],
7485            })
7486        );
7487        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7488
7489        let point_ctor = PointCtor {
7490            position: Point2d {
7491                x: Expr::Number(Number {
7492                    value: 1.0,
7493                    units: NumericSuffix::Inch,
7494                }),
7495                y: Expr::Number(Number {
7496                    value: 2.0,
7497                    units: NumericSuffix::Inch,
7498                }),
7499            },
7500        };
7501        let segment = SegmentCtor::Point(point_ctor);
7502        let (src_delta, scene_delta) = frontend
7503            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7504            .await
7505            .unwrap();
7506        assert_eq!(
7507            src_delta.text.as_str(),
7508            "sketch001 = sketch(on = XY) {
7509  point(at = [1in, 2in])
7510}
7511"
7512        );
7513        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7514        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7515        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7516            assert_eq!(scene_object.id.0, i);
7517        }
7518
7519        let point_id = *scene_delta.new_objects.last().unwrap();
7520
7521        let point_ctor = PointCtor {
7522            position: Point2d {
7523                x: Expr::Number(Number {
7524                    value: 3.0,
7525                    units: NumericSuffix::Inch,
7526                }),
7527                y: Expr::Number(Number {
7528                    value: 4.0,
7529                    units: NumericSuffix::Inch,
7530                }),
7531            },
7532        };
7533        let segments = vec![ExistingSegmentCtor {
7534            id: point_id,
7535            ctor: SegmentCtor::Point(point_ctor),
7536        }];
7537        let (src_delta, scene_delta) = frontend
7538            .edit_segments(&mock_ctx, version, sketch_id, segments)
7539            .await
7540            .unwrap();
7541        assert_eq!(
7542            src_delta.text.as_str(),
7543            "sketch001 = sketch(on = XY) {
7544  point(at = [3in, 4in])
7545}
7546"
7547        );
7548        assert_eq!(scene_delta.new_objects, vec![]);
7549        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7550
7551        ctx.close().await;
7552        mock_ctx.close().await;
7553    }
7554
7555    #[tokio::test(flavor = "multi_thread")]
7556    async fn test_new_sketch_add_line_edit_line() {
7557        let program = Program::empty();
7558
7559        let mut frontend = FrontendState::new();
7560        frontend.program = program;
7561
7562        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7563        let mock_ctx = ExecutorContext::new_mock(None).await;
7564        let version = Version(0);
7565
7566        let sketch_args = SketchCtor {
7567            on: Plane::Default(PlaneName::Xy),
7568        };
7569        let (_src_delta, scene_delta, sketch_id) = frontend
7570            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7571            .await
7572            .unwrap();
7573        assert_eq!(sketch_id, ObjectId(1));
7574        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7575        let sketch_object = &scene_delta.new_graph.objects[1];
7576        assert_eq!(sketch_object.id, ObjectId(1));
7577        assert_eq!(
7578            sketch_object.kind,
7579            ObjectKind::Sketch(Sketch {
7580                args: SketchCtor {
7581                    on: Plane::Default(PlaneName::Xy)
7582                },
7583                plane: ObjectId(0),
7584                segments: vec![],
7585                constraints: vec![],
7586            })
7587        );
7588        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7589
7590        let line_ctor = LineCtor {
7591            start: Point2d {
7592                x: Expr::Number(Number {
7593                    value: 0.0,
7594                    units: NumericSuffix::Mm,
7595                }),
7596                y: Expr::Number(Number {
7597                    value: 0.0,
7598                    units: NumericSuffix::Mm,
7599                }),
7600            },
7601            end: Point2d {
7602                x: Expr::Number(Number {
7603                    value: 10.0,
7604                    units: NumericSuffix::Mm,
7605                }),
7606                y: Expr::Number(Number {
7607                    value: 10.0,
7608                    units: NumericSuffix::Mm,
7609                }),
7610            },
7611            construction: None,
7612        };
7613        let segment = SegmentCtor::Line(line_ctor);
7614        let (src_delta, scene_delta) = frontend
7615            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7616            .await
7617            .unwrap();
7618        assert_eq!(
7619            src_delta.text.as_str(),
7620            "sketch001 = sketch(on = XY) {
7621  line(start = [0mm, 0mm], end = [10mm, 10mm])
7622}
7623"
7624        );
7625        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7626        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7627        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7628            assert_eq!(scene_object.id.0, i);
7629        }
7630
7631        // The new objects are the end points and then the line.
7632        let line = *scene_delta.new_objects.last().unwrap();
7633
7634        let line_ctor = LineCtor {
7635            start: Point2d {
7636                x: Expr::Number(Number {
7637                    value: 1.0,
7638                    units: NumericSuffix::Mm,
7639                }),
7640                y: Expr::Number(Number {
7641                    value: 2.0,
7642                    units: NumericSuffix::Mm,
7643                }),
7644            },
7645            end: Point2d {
7646                x: Expr::Number(Number {
7647                    value: 13.0,
7648                    units: NumericSuffix::Mm,
7649                }),
7650                y: Expr::Number(Number {
7651                    value: 14.0,
7652                    units: NumericSuffix::Mm,
7653                }),
7654            },
7655            construction: None,
7656        };
7657        let segments = vec![ExistingSegmentCtor {
7658            id: line,
7659            ctor: SegmentCtor::Line(line_ctor),
7660        }];
7661        let (src_delta, scene_delta) = frontend
7662            .edit_segments(&mock_ctx, version, sketch_id, segments)
7663            .await
7664            .unwrap();
7665        assert_eq!(
7666            src_delta.text.as_str(),
7667            "sketch001 = sketch(on = XY) {
7668  line(start = [1mm, 2mm], end = [13mm, 14mm])
7669}
7670"
7671        );
7672        assert_eq!(scene_delta.new_objects, vec![]);
7673        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7674
7675        ctx.close().await;
7676        mock_ctx.close().await;
7677    }
7678
7679    #[tokio::test(flavor = "multi_thread")]
7680    async fn test_new_sketch_add_arc_edit_arc() {
7681        let program = Program::empty();
7682
7683        let mut frontend = FrontendState::new();
7684        frontend.program = program;
7685
7686        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7687        let mock_ctx = ExecutorContext::new_mock(None).await;
7688        let version = Version(0);
7689
7690        let sketch_args = SketchCtor {
7691            on: Plane::Default(PlaneName::Xy),
7692        };
7693        let (_src_delta, scene_delta, sketch_id) = frontend
7694            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7695            .await
7696            .unwrap();
7697        assert_eq!(sketch_id, ObjectId(1));
7698        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7699        let sketch_object = &scene_delta.new_graph.objects[1];
7700        assert_eq!(sketch_object.id, ObjectId(1));
7701        assert_eq!(
7702            sketch_object.kind,
7703            ObjectKind::Sketch(Sketch {
7704                args: SketchCtor {
7705                    on: Plane::Default(PlaneName::Xy),
7706                },
7707                plane: ObjectId(0),
7708                segments: vec![],
7709                constraints: vec![],
7710            })
7711        );
7712        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7713
7714        let arc_ctor = ArcCtor {
7715            start: Point2d {
7716                x: Expr::Var(Number {
7717                    value: 0.0,
7718                    units: NumericSuffix::Mm,
7719                }),
7720                y: Expr::Var(Number {
7721                    value: 0.0,
7722                    units: NumericSuffix::Mm,
7723                }),
7724            },
7725            end: Point2d {
7726                x: Expr::Var(Number {
7727                    value: 10.0,
7728                    units: NumericSuffix::Mm,
7729                }),
7730                y: Expr::Var(Number {
7731                    value: 10.0,
7732                    units: NumericSuffix::Mm,
7733                }),
7734            },
7735            center: Point2d {
7736                x: Expr::Var(Number {
7737                    value: 10.0,
7738                    units: NumericSuffix::Mm,
7739                }),
7740                y: Expr::Var(Number {
7741                    value: 0.0,
7742                    units: NumericSuffix::Mm,
7743                }),
7744            },
7745            construction: None,
7746        };
7747        let segment = SegmentCtor::Arc(arc_ctor);
7748        let (src_delta, scene_delta) = frontend
7749            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7750            .await
7751            .unwrap();
7752        assert_eq!(
7753            src_delta.text.as_str(),
7754            "sketch001 = sketch(on = XY) {
7755  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7756}
7757"
7758        );
7759        assert_eq!(
7760            scene_delta.new_objects,
7761            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7762        );
7763        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7764            assert_eq!(scene_object.id.0, i);
7765        }
7766        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7767
7768        // The new objects are the end points, the center, and then the arc.
7769        let arc = *scene_delta.new_objects.last().unwrap();
7770
7771        let arc_ctor = ArcCtor {
7772            start: Point2d {
7773                x: Expr::Var(Number {
7774                    value: 1.0,
7775                    units: NumericSuffix::Mm,
7776                }),
7777                y: Expr::Var(Number {
7778                    value: 2.0,
7779                    units: NumericSuffix::Mm,
7780                }),
7781            },
7782            end: Point2d {
7783                x: Expr::Var(Number {
7784                    value: 13.0,
7785                    units: NumericSuffix::Mm,
7786                }),
7787                y: Expr::Var(Number {
7788                    value: 14.0,
7789                    units: NumericSuffix::Mm,
7790                }),
7791            },
7792            center: Point2d {
7793                x: Expr::Var(Number {
7794                    value: 13.0,
7795                    units: NumericSuffix::Mm,
7796                }),
7797                y: Expr::Var(Number {
7798                    value: 2.0,
7799                    units: NumericSuffix::Mm,
7800                }),
7801            },
7802            construction: None,
7803        };
7804        let segments = vec![ExistingSegmentCtor {
7805            id: arc,
7806            ctor: SegmentCtor::Arc(arc_ctor),
7807        }];
7808        let (src_delta, scene_delta) = frontend
7809            .edit_segments(&mock_ctx, version, sketch_id, segments)
7810            .await
7811            .unwrap();
7812        assert_eq!(
7813            src_delta.text.as_str(),
7814            "sketch001 = sketch(on = XY) {
7815  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7816}
7817"
7818        );
7819        assert_eq!(scene_delta.new_objects, vec![]);
7820        assert_eq!(scene_delta.new_graph.objects.len(), 6);
7821
7822        ctx.close().await;
7823        mock_ctx.close().await;
7824    }
7825
7826    #[tokio::test(flavor = "multi_thread")]
7827    async fn test_new_sketch_add_circle_edit_circle() {
7828        let program = Program::empty();
7829
7830        let mut frontend = FrontendState::new();
7831        frontend.program = program;
7832
7833        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7834        let mock_ctx = ExecutorContext::new_mock(None).await;
7835        let version = Version(0);
7836
7837        let sketch_args = SketchCtor {
7838            on: Plane::Default(PlaneName::Xy),
7839        };
7840        let (_src_delta, _scene_delta, sketch_id) = frontend
7841            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7842            .await
7843            .unwrap();
7844
7845        // Add a circle segment.
7846        let circle_ctor = CircleCtor {
7847            start: Point2d {
7848                x: Expr::Var(Number {
7849                    value: 5.0,
7850                    units: NumericSuffix::Mm,
7851                }),
7852                y: Expr::Var(Number {
7853                    value: 0.0,
7854                    units: NumericSuffix::Mm,
7855                }),
7856            },
7857            center: Point2d {
7858                x: Expr::Var(Number {
7859                    value: 0.0,
7860                    units: NumericSuffix::Mm,
7861                }),
7862                y: Expr::Var(Number {
7863                    value: 0.0,
7864                    units: NumericSuffix::Mm,
7865                }),
7866            },
7867            construction: None,
7868        };
7869        let segment = SegmentCtor::Circle(circle_ctor);
7870        let (src_delta, scene_delta) = frontend
7871            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7872            .await
7873            .unwrap();
7874        assert_eq!(
7875            src_delta.text.as_str(),
7876            "sketch001 = sketch(on = XY) {
7877  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7878}
7879"
7880        );
7881        // The new objects are start, center, and then the circle segment.
7882        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7883        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7884
7885        let circle = *scene_delta.new_objects.last().unwrap();
7886
7887        // Edit the circle segment.
7888        let circle_ctor = CircleCtor {
7889            start: Point2d {
7890                x: Expr::Var(Number {
7891                    value: 10.0,
7892                    units: NumericSuffix::Mm,
7893                }),
7894                y: Expr::Var(Number {
7895                    value: 0.0,
7896                    units: NumericSuffix::Mm,
7897                }),
7898            },
7899            center: Point2d {
7900                x: Expr::Var(Number {
7901                    value: 3.0,
7902                    units: NumericSuffix::Mm,
7903                }),
7904                y: Expr::Var(Number {
7905                    value: 4.0,
7906                    units: NumericSuffix::Mm,
7907                }),
7908            },
7909            construction: None,
7910        };
7911        let segments = vec![ExistingSegmentCtor {
7912            id: circle,
7913            ctor: SegmentCtor::Circle(circle_ctor),
7914        }];
7915        let (src_delta, scene_delta) = frontend
7916            .edit_segments(&mock_ctx, version, sketch_id, segments)
7917            .await
7918            .unwrap();
7919        assert_eq!(
7920            src_delta.text.as_str(),
7921            "sketch001 = sketch(on = XY) {
7922  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7923}
7924"
7925        );
7926        assert_eq!(scene_delta.new_objects, vec![]);
7927        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7928
7929        ctx.close().await;
7930        mock_ctx.close().await;
7931    }
7932
7933    #[tokio::test(flavor = "multi_thread")]
7934    async fn test_delete_circle() {
7935        let initial_source = "sketch001 = sketch(on = XY) {
7936  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7937}
7938";
7939
7940        let program = Program::parse(initial_source).unwrap().0.unwrap();
7941        let mut frontend = FrontendState::new();
7942
7943        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7944        let mock_ctx = ExecutorContext::new_mock(None).await;
7945        let version = Version(0);
7946
7947        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7948        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7949        let sketch_id = sketch_object.id;
7950        let sketch = expect_sketch(sketch_object);
7951
7952        // The sketch should have 3 segments: start point, center point, and the circle.
7953        assert_eq!(sketch.segments.len(), 3);
7954        let circle_id = sketch.segments[2];
7955
7956        // Delete the circle.
7957        let (src_delta, scene_delta) = frontend
7958            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7959            .await
7960            .unwrap();
7961        assert_eq!(
7962            src_delta.text.as_str(),
7963            "sketch001 = sketch(on = XY) {
7964}
7965"
7966        );
7967        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7968        let new_sketch = expect_sketch(new_sketch_object);
7969        assert_eq!(new_sketch.segments.len(), 0);
7970
7971        ctx.close().await;
7972        mock_ctx.close().await;
7973    }
7974
7975    #[tokio::test(flavor = "multi_thread")]
7976    async fn test_edit_circle_via_point() {
7977        let initial_source = "sketch001 = sketch(on = XY) {
7978  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7979}
7980";
7981
7982        let program = Program::parse(initial_source).unwrap().0.unwrap();
7983        let mut frontend = FrontendState::new();
7984
7985        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7986        let mock_ctx = ExecutorContext::new_mock(None).await;
7987        let version = Version(0);
7988
7989        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7990        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7991        let sketch_id = sketch_object.id;
7992        let sketch = expect_sketch(sketch_object);
7993
7994        // Find the circle segment and its start point.
7995        let circle_id = sketch
7996            .segments
7997            .iter()
7998            .copied()
7999            .find(|seg_id| {
8000                matches!(
8001                    &frontend.scene_graph.objects[seg_id.0].kind,
8002                    ObjectKind::Segment {
8003                        segment: Segment::Circle(_)
8004                    }
8005                )
8006            })
8007            .expect("Expected a circle segment in sketch");
8008        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8009        let ObjectKind::Segment {
8010            segment: Segment::Circle(circle),
8011        } = &circle_object.kind
8012        else {
8013            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8014        };
8015        let start_point_id = circle.start;
8016
8017        // Edit the start point via SegmentCtor::Point.
8018        let segments = vec![ExistingSegmentCtor {
8019            id: start_point_id,
8020            ctor: SegmentCtor::Point(PointCtor {
8021                position: Point2d {
8022                    x: Expr::Var(Number {
8023                        value: 7.0,
8024                        units: NumericSuffix::Mm,
8025                    }),
8026                    y: Expr::Var(Number {
8027                        value: 1.0,
8028                        units: NumericSuffix::Mm,
8029                    }),
8030                },
8031            }),
8032        }];
8033        let (src_delta, _scene_delta) = frontend
8034            .edit_segments(&mock_ctx, version, sketch_id, segments)
8035            .await
8036            .unwrap();
8037        assert_eq!(
8038            src_delta.text.as_str(),
8039            "sketch001 = sketch(on = XY) {
8040  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8041}
8042"
8043        );
8044
8045        ctx.close().await;
8046        mock_ctx.close().await;
8047    }
8048
8049    #[tokio::test(flavor = "multi_thread")]
8050    async fn test_add_line_when_sketch_block_uses_variable() {
8051        let initial_source = "s = sketch(on = XY) {}
8052";
8053
8054        let program = Program::parse(initial_source).unwrap().0.unwrap();
8055
8056        let mut frontend = FrontendState::new();
8057
8058        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8059        let mock_ctx = ExecutorContext::new_mock(None).await;
8060        let version = Version(0);
8061
8062        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8063        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8064        let sketch_id = sketch_object.id;
8065
8066        let line_ctor = LineCtor {
8067            start: Point2d {
8068                x: Expr::Number(Number {
8069                    value: 0.0,
8070                    units: NumericSuffix::Mm,
8071                }),
8072                y: Expr::Number(Number {
8073                    value: 0.0,
8074                    units: NumericSuffix::Mm,
8075                }),
8076            },
8077            end: Point2d {
8078                x: Expr::Number(Number {
8079                    value: 10.0,
8080                    units: NumericSuffix::Mm,
8081                }),
8082                y: Expr::Number(Number {
8083                    value: 10.0,
8084                    units: NumericSuffix::Mm,
8085                }),
8086            },
8087            construction: None,
8088        };
8089        let segment = SegmentCtor::Line(line_ctor);
8090        let (src_delta, scene_delta) = frontend
8091            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8092            .await
8093            .unwrap();
8094        assert_eq!(
8095            src_delta.text.as_str(),
8096            "s = sketch(on = XY) {
8097  line(start = [0mm, 0mm], end = [10mm, 10mm])
8098}
8099"
8100        );
8101        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8102        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8103
8104        ctx.close().await;
8105        mock_ctx.close().await;
8106    }
8107
8108    #[tokio::test(flavor = "multi_thread")]
8109    async fn test_new_sketch_add_line_delete_sketch() {
8110        let program = Program::empty();
8111
8112        let mut frontend = FrontendState::new();
8113        frontend.program = program;
8114
8115        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8116        let mock_ctx = ExecutorContext::new_mock(None).await;
8117        let version = Version(0);
8118
8119        let sketch_args = SketchCtor {
8120            on: Plane::Default(PlaneName::Xy),
8121        };
8122        let (_src_delta, scene_delta, sketch_id) = frontend
8123            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8124            .await
8125            .unwrap();
8126        assert_eq!(sketch_id, ObjectId(1));
8127        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8128        let sketch_object = &scene_delta.new_graph.objects[1];
8129        assert_eq!(sketch_object.id, ObjectId(1));
8130        assert_eq!(
8131            sketch_object.kind,
8132            ObjectKind::Sketch(Sketch {
8133                args: SketchCtor {
8134                    on: Plane::Default(PlaneName::Xy)
8135                },
8136                plane: ObjectId(0),
8137                segments: vec![],
8138                constraints: vec![],
8139            })
8140        );
8141        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8142
8143        let line_ctor = LineCtor {
8144            start: Point2d {
8145                x: Expr::Number(Number {
8146                    value: 0.0,
8147                    units: NumericSuffix::Mm,
8148                }),
8149                y: Expr::Number(Number {
8150                    value: 0.0,
8151                    units: NumericSuffix::Mm,
8152                }),
8153            },
8154            end: Point2d {
8155                x: Expr::Number(Number {
8156                    value: 10.0,
8157                    units: NumericSuffix::Mm,
8158                }),
8159                y: Expr::Number(Number {
8160                    value: 10.0,
8161                    units: NumericSuffix::Mm,
8162                }),
8163            },
8164            construction: None,
8165        };
8166        let segment = SegmentCtor::Line(line_ctor);
8167        let (src_delta, scene_delta) = frontend
8168            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8169            .await
8170            .unwrap();
8171        assert_eq!(
8172            src_delta.text.as_str(),
8173            "sketch001 = sketch(on = XY) {
8174  line(start = [0mm, 0mm], end = [10mm, 10mm])
8175}
8176"
8177        );
8178        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8179
8180        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8181        assert_eq!(src_delta.text.as_str(), "");
8182        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8183
8184        ctx.close().await;
8185        mock_ctx.close().await;
8186    }
8187
8188    #[tokio::test(flavor = "multi_thread")]
8189    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8190        let initial_source = "s = sketch(on = XY) {}
8191";
8192
8193        let program = Program::parse(initial_source).unwrap().0.unwrap();
8194
8195        let mut frontend = FrontendState::new();
8196
8197        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8198        let version = Version(0);
8199
8200        frontend.hack_set_program(&ctx, program).await.unwrap();
8201        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202        let sketch_id = sketch_object.id;
8203
8204        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8205        assert_eq!(src_delta.text.as_str(), "");
8206        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8207
8208        ctx.close().await;
8209    }
8210
8211    #[tokio::test(flavor = "multi_thread")]
8212    async fn test_delete_sketch_after_comment() {
8213        let initial_source = "sketch001 = sketch(on = XZ) {
8214}
8215";
8216
8217        let program = Program::parse(initial_source).unwrap().0.unwrap();
8218        let mut frontend = FrontendState::new();
8219
8220        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8221        let version = Version(0);
8222
8223        frontend.hack_set_program(&ctx, program).await.unwrap();
8224        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8225        let sketch_id = sketch_object.id;
8226        let original_source = sketch_object.source.clone();
8227
8228        let commented_source = "// test 1
8229sketch001 = sketch(on = XZ) {
8230}
8231";
8232        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8233        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8234
8235        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8236        assert_eq!(cached_sketch_object.source, original_source);
8237
8238        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8239        assert!(
8240            !src_delta.text.contains("sketch001"),
8241            "sketch was not deleted: {}",
8242            src_delta.text
8243        );
8244        // The leading line comment must survive deletion.
8245        assert_eq!(src_delta.text.as_str(), "// test 1\n");
8246        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8247
8248        ctx.close().await;
8249    }
8250
8251    #[tokio::test(flavor = "multi_thread")]
8252    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8253        let initial_source = "sketch001 = sketch(on = XZ) {
8254}
8255foo = 1
8256";
8257
8258        let program = Program::parse(initial_source).unwrap().0.unwrap();
8259        let mut frontend = FrontendState::new();
8260
8261        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8262        let version = Version(0);
8263
8264        frontend.hack_set_program(&ctx, program).await.unwrap();
8265        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8266        let sketch_id = sketch_object.id;
8267
8268        let commented_source = "// keep me
8269sketch001 = sketch(on = XZ) {
8270}
8271foo = 1
8272";
8273        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8274        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8275
8276        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8277        // The leading comment should remain, now attached to the following body item.
8278        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8279
8280        ctx.close().await;
8281    }
8282
8283    #[tokio::test(flavor = "multi_thread")]
8284    async fn test_delete_segment_preserves_pre_comment() {
8285        let initial_source = "\
8286sketch(on = XY) {
8287  point(at = [var 1, var 2])
8288  // describe the middle point
8289  point(at = [var 3, var 4])
8290  point(at = [var 5, var 6])
8291}
8292";
8293
8294        let program = Program::parse(initial_source).unwrap().0.unwrap();
8295        let mut frontend = FrontendState::new();
8296
8297        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8298        let mock_ctx = ExecutorContext::new_mock(None).await;
8299        let version = Version(0);
8300
8301        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8302        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8303        let sketch_id = sketch_object.id;
8304        let sketch = expect_sketch(sketch_object);
8305
8306        let middle_point_id = *sketch.segments.get(1).unwrap();
8307
8308        let (src_delta, _scene_delta) = frontend
8309            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8310            .await
8311            .unwrap();
8312        // The line comment on the line above the deleted point must be preserved.
8313        // It is reattached to the next surviving body item.
8314        assert_eq!(
8315            src_delta.text.as_str(),
8316            "\
8317sketch(on = XY) {
8318  point(at = [var 1, var 2])
8319  // describe the middle point
8320  point(at = [var 5, var 6])
8321}
8322"
8323        );
8324
8325        ctx.close().await;
8326        mock_ctx.close().await;
8327    }
8328
8329    #[tokio::test(flavor = "multi_thread")]
8330    async fn test_delete_last_segment_preserves_pre_comment() {
8331        let initial_source = "\
8332sketch(on = XY) {
8333  point(at = [var 1, var 2])
8334  // describe the trailing point
8335  point(at = [var 3, var 4])
8336}
8337";
8338
8339        let program = Program::parse(initial_source).unwrap().0.unwrap();
8340        let mut frontend = FrontendState::new();
8341
8342        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8343        let mock_ctx = ExecutorContext::new_mock(None).await;
8344        let version = Version(0);
8345
8346        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8347        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8348        let sketch_id = sketch_object.id;
8349        let sketch = expect_sketch(sketch_object);
8350
8351        let last_point_id = *sketch.segments.last().unwrap();
8352
8353        let (src_delta, _scene_delta) = frontend
8354            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8355            .await
8356            .unwrap();
8357        // No following item to attach to; the comment is kept inside the sketch
8358        // block as trailing non-code metadata so the user does not lose it.
8359        assert_eq!(
8360            src_delta.text.as_str(),
8361            "\
8362sketch(on = XY) {
8363  point(at = [var 1, var 2])
8364  // describe the trailing point
8365}
8366"
8367        );
8368
8369        ctx.close().await;
8370        mock_ctx.close().await;
8371    }
8372
8373    #[tokio::test(flavor = "multi_thread")]
8374    async fn test_delete_segment_drops_inline_trailing_comment() {
8375        let initial_source = "\
8376sketch(on = XY) {
8377  point(at = [var 1, var 2])
8378  point(at = [var 3, var 4]) // same-line note that gets dropped
8379  point(at = [var 5, var 6])
8380}
8381";
8382
8383        let program = Program::parse(initial_source).unwrap().0.unwrap();
8384        let mut frontend = FrontendState::new();
8385
8386        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8387        let mock_ctx = ExecutorContext::new_mock(None).await;
8388        let version = Version(0);
8389
8390        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8391        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8392        let sketch_id = sketch_object.id;
8393        let sketch = expect_sketch(sketch_object);
8394
8395        let middle_point_id = *sketch.segments.get(1).unwrap();
8396
8397        let (src_delta, _scene_delta) = frontend
8398            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8399            .await
8400            .unwrap();
8401        // The same-line trailing comment is removed along with the deleted code.
8402        assert!(
8403            !src_delta.text.contains("same-line note"),
8404            "inline comment should have been removed: {}",
8405            src_delta.text
8406        );
8407
8408        ctx.close().await;
8409        mock_ctx.close().await;
8410    }
8411
8412    #[tokio::test(flavor = "multi_thread")]
8413    async fn test_delete_segments_preserves_block_comments_across_positions() {
8414        // One test exercising several `delete_body_item_preserving_pre_comments`
8415        // branches at once with `/* ... */` block comments:
8416        //   - first point: leading block comment must migrate to the next item.
8417        //   - first point: same-line trailing block comment must be dropped.
8418        //   - middle point: leading block comment must stay attached after migration.
8419        //   - last point: leading block comment, with no surviving next item,
8420        //     must be converted into a trailing NonCodeNode.
8421        let initial_source = "\
8422sketch(on = XY) {
8423  /* above first - moves to middle */
8424  point(at = [var 1, var 2]) /* same-line on first - dropped */
8425  /* above middle - stays */
8426  point(at = [var 3, var 4])
8427  /* above last - moves to trailing meta */
8428  point(at = [var 5, var 6])
8429}
8430";
8431
8432        let program = Program::parse(initial_source).unwrap().0.unwrap();
8433        let mut frontend = FrontendState::new();
8434
8435        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8436        let mock_ctx = ExecutorContext::new_mock(None).await;
8437        let version = Version(0);
8438
8439        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8440        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8441        let sketch_id = sketch_object.id;
8442        let sketch = expect_sketch(sketch_object);
8443
8444        let first_point_id = *sketch.segments.first().unwrap();
8445        let last_point_id = *sketch.segments.last().unwrap();
8446
8447        let (src_delta, _scene_delta) = frontend
8448            .delete_objects(
8449                &mock_ctx,
8450                version,
8451                sketch_id,
8452                Vec::new(),
8453                vec![first_point_id, last_point_id],
8454            )
8455            .await
8456            .unwrap();
8457        assert_eq!(
8458            src_delta.text.as_str(),
8459            "\
8460sketch(on = XY) {
8461  /* above first - moves to middle */
8462  /* above middle - stays */
8463  point(at = [var 3, var 4])
8464  /* above last - moves to trailing meta */
8465}
8466"
8467        );
8468
8469        ctx.close().await;
8470        mock_ctx.close().await;
8471    }
8472
8473    #[tokio::test(flavor = "multi_thread")]
8474    async fn test_edit_line_when_editing_its_start_point() {
8475        let initial_source = "\
8476sketch(on = XY) {
8477  line(start = [var 1, var 2], end = [var 3, var 4])
8478}
8479";
8480
8481        let program = Program::parse(initial_source).unwrap().0.unwrap();
8482
8483        let mut frontend = FrontendState::new();
8484
8485        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8486        let mock_ctx = ExecutorContext::new_mock(None).await;
8487        let version = Version(0);
8488
8489        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8490        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8491        let sketch_id = sketch_object.id;
8492        let sketch = expect_sketch(sketch_object);
8493
8494        let point_id = *sketch.segments.first().unwrap();
8495
8496        let point_ctor = PointCtor {
8497            position: Point2d {
8498                x: Expr::Var(Number {
8499                    value: 5.0,
8500                    units: NumericSuffix::Inch,
8501                }),
8502                y: Expr::Var(Number {
8503                    value: 6.0,
8504                    units: NumericSuffix::Inch,
8505                }),
8506            },
8507        };
8508        let segments = vec![ExistingSegmentCtor {
8509            id: point_id,
8510            ctor: SegmentCtor::Point(point_ctor),
8511        }];
8512        let (src_delta, scene_delta) = frontend
8513            .edit_segments(&mock_ctx, version, sketch_id, segments)
8514            .await
8515            .unwrap();
8516        assert_eq!(
8517            src_delta.text.as_str(),
8518            "\
8519sketch(on = XY) {
8520  line(start = [var 5in, var 6in], end = [var 3, var 4])
8521}
8522"
8523        );
8524        assert_eq!(scene_delta.new_objects, vec![]);
8525        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8526
8527        ctx.close().await;
8528        mock_ctx.close().await;
8529    }
8530
8531    #[tokio::test(flavor = "multi_thread")]
8532    async fn test_edit_line_when_editing_its_end_point() {
8533        let initial_source = "\
8534sketch(on = XY) {
8535  line(start = [var 1, var 2], end = [var 3, var 4])
8536}
8537";
8538
8539        let program = Program::parse(initial_source).unwrap().0.unwrap();
8540
8541        let mut frontend = FrontendState::new();
8542
8543        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8544        let mock_ctx = ExecutorContext::new_mock(None).await;
8545        let version = Version(0);
8546
8547        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8548        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8549        let sketch_id = sketch_object.id;
8550        let sketch = expect_sketch(sketch_object);
8551        let point_id = *sketch.segments.get(1).unwrap();
8552
8553        let point_ctor = PointCtor {
8554            position: Point2d {
8555                x: Expr::Var(Number {
8556                    value: 5.0,
8557                    units: NumericSuffix::Inch,
8558                }),
8559                y: Expr::Var(Number {
8560                    value: 6.0,
8561                    units: NumericSuffix::Inch,
8562                }),
8563            },
8564        };
8565        let segments = vec![ExistingSegmentCtor {
8566            id: point_id,
8567            ctor: SegmentCtor::Point(point_ctor),
8568        }];
8569        let (src_delta, scene_delta) = frontend
8570            .edit_segments(&mock_ctx, version, sketch_id, segments)
8571            .await
8572            .unwrap();
8573        assert_eq!(
8574            src_delta.text.as_str(),
8575            "\
8576sketch(on = XY) {
8577  line(start = [var 1, var 2], end = [var 5in, var 6in])
8578}
8579"
8580        );
8581        assert_eq!(scene_delta.new_objects, vec![]);
8582        assert_eq!(
8583            scene_delta.new_graph.objects.len(),
8584            5,
8585            "{:#?}",
8586            scene_delta.new_graph.objects
8587        );
8588
8589        ctx.close().await;
8590        mock_ctx.close().await;
8591    }
8592
8593    #[tokio::test(flavor = "multi_thread")]
8594    async fn test_edit_line_with_coincident_feedback() {
8595        let initial_source = "\
8596sketch(on = XY) {
8597  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8598  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8599  fixed([line1.start, [0, 0]])
8600  coincident([line1.end, line2.start])
8601  equalLength([line1, line2])
8602}
8603";
8604
8605        let program = Program::parse(initial_source).unwrap().0.unwrap();
8606
8607        let mut frontend = FrontendState::new();
8608
8609        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8610        let mock_ctx = ExecutorContext::new_mock(None).await;
8611        let version = Version(0);
8612
8613        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8614        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8615        let sketch_id = sketch_object.id;
8616        let sketch = expect_sketch(sketch_object);
8617        let line2_end_id = *sketch.segments.get(4).unwrap();
8618
8619        let segments = vec![ExistingSegmentCtor {
8620            id: line2_end_id,
8621            ctor: SegmentCtor::Point(PointCtor {
8622                position: Point2d {
8623                    x: Expr::Var(Number {
8624                        value: 9.0,
8625                        units: NumericSuffix::None,
8626                    }),
8627                    y: Expr::Var(Number {
8628                        value: 10.0,
8629                        units: NumericSuffix::None,
8630                    }),
8631                },
8632            }),
8633        }];
8634        let (src_delta, scene_delta) = frontend
8635            .edit_segments(&mock_ctx, version, sketch_id, segments)
8636            .await
8637            .unwrap();
8638        assert_eq!(
8639            src_delta.text.as_str(),
8640            "\
8641sketch(on = XY) {
8642  line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8643  line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8644  fixed([line1.start, [0, 0]])
8645  coincident([line1.end, line2.start])
8646  equalLength([line1, line2])
8647}
8648"
8649        );
8650        assert_eq!(
8651            scene_delta.new_graph.objects.len(),
8652            11,
8653            "{:#?}",
8654            scene_delta.new_graph.objects
8655        );
8656
8657        ctx.close().await;
8658        mock_ctx.close().await;
8659    }
8660
8661    #[tokio::test(flavor = "multi_thread")]
8662    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8663        let initial_source = "\
8664sketch(on = XY) {
8665  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8666  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8667  fixed([line1.start, [0, 0]])
8668  coincident([line1.end, line2.start])
8669  equalLength([line1, line2])
8670}
8671";
8672
8673        let program = Program::parse(initial_source).unwrap().0.unwrap();
8674        let mut frontend = FrontendState::new();
8675        let mock_ctx = ExecutorContext::new_mock(None).await;
8676        let version = Version(0);
8677
8678        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8679        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8680        let sketch_id = sketch_object.id;
8681        let sketch = expect_sketch(sketch_object);
8682        let line2_end_id = *sketch.segments.get(4).unwrap();
8683
8684        let segments = vec![ExistingSegmentCtor {
8685            id: line2_end_id,
8686            ctor: SegmentCtor::Point(PointCtor {
8687                position: Point2d {
8688                    x: Expr::Var(Number {
8689                        value: 9.0,
8690                        units: NumericSuffix::None,
8691                    }),
8692                    y: Expr::Var(Number {
8693                        value: 10.0,
8694                        units: NumericSuffix::None,
8695                    }),
8696                },
8697            }),
8698        }];
8699        let (edited_source, _) = frontend
8700            .edit_segments(&mock_ctx, version, sketch_id, segments)
8701            .await
8702            .unwrap();
8703
8704        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8705        assert_eq!(mock_source.text, edited_source.text);
8706
8707        mock_ctx.close().await;
8708    }
8709
8710    /// Preview segment edits should return solved geometry without persisting
8711    /// solver feedback to KCL.
8712    #[tokio::test(flavor = "multi_thread")]
8713    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8714        let initial_source = "\
8715sketch(on = XY) {
8716  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8717  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8718  fixed([line1.start, [0, 0]])
8719  coincident([line1.end, line2.start])
8720  equalLength([line1, line2])
8721}
8722";
8723
8724        let program = Program::parse(initial_source).unwrap().0.unwrap();
8725        let mut frontend = FrontendState::new();
8726        let mock_ctx = ExecutorContext::new_mock(None).await;
8727        let version = Version(0);
8728
8729        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8730        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8731        let sketch_id = sketch_object.id;
8732        let sketch = expect_sketch(sketch_object);
8733        let line2_end_id = *sketch.segments.get(4).unwrap();
8734
8735        let segments = vec![ExistingSegmentCtor {
8736            id: line2_end_id,
8737            ctor: SegmentCtor::Point(PointCtor {
8738                position: Point2d {
8739                    x: Expr::Var(Number {
8740                        value: 9.0,
8741                        units: NumericSuffix::None,
8742                    }),
8743                    y: Expr::Var(Number {
8744                        value: 10.0,
8745                        units: NumericSuffix::None,
8746                    }),
8747                },
8748            }),
8749        }];
8750        let (preview_source, preview_delta) = frontend
8751            .edit_segments_with_options(
8752                &mock_ctx,
8753                version,
8754                sketch_id,
8755                segments,
8756                EditSegmentsOptions {
8757                    anchor_segment_ids: Some(vec![line2_end_id]),
8758                    drag_anchors: Vec::new(),
8759                    commit_solved_initial_guesses: false,
8760                },
8761            )
8762            .await
8763            .unwrap();
8764
8765        assert!(
8766            !preview_delta.exec_outcome.var_solutions.is_empty(),
8767            "preview solve should still solve and return geometry feedback"
8768        );
8769        assert!(
8770            preview_source
8771                .text
8772                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8773        );
8774        assert!(
8775            preview_source
8776                .text
8777                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8778        );
8779
8780        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8781        assert_eq!(mock_source.text, preview_source.text);
8782
8783        mock_ctx.close().await;
8784    }
8785
8786    #[tokio::test(flavor = "multi_thread")]
8787    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8788        let initial_source = "\
8789sketch(on = XY) {
8790  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8791}
8792";
8793
8794        let program = Program::parse(initial_source).unwrap().0.unwrap();
8795        let mut frontend = FrontendState::new();
8796        let mock_ctx = ExecutorContext::new_mock(None).await;
8797        let version = Version(0);
8798
8799        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8801        let sketch_id = sketch_object.id;
8802        let sketch = expect_sketch(sketch_object);
8803        let line_end_id = *sketch.segments.get(1).unwrap();
8804
8805        let constraint = Constraint::Fixed(Fixed {
8806            points: vec![FixedPoint {
8807                point: line_end_id,
8808                position: Point2d {
8809                    x: Number {
8810                        value: 20.0,
8811                        units: NumericSuffix::Mm,
8812                    },
8813                    y: Number {
8814                        value: 0.0,
8815                        units: NumericSuffix::Mm,
8816                    },
8817                },
8818            }],
8819        });
8820        let (constraint_source, _) = frontend
8821            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8822            .await
8823            .unwrap();
8824
8825        assert!(
8826            constraint_source
8827                .text
8828                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8829            "{}",
8830            constraint_source.text
8831        );
8832        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8833        assert_eq!(mock_source.text, constraint_source.text);
8834
8835        mock_ctx.close().await;
8836    }
8837
8838    #[test]
8839    fn test_no_solver_feedback_preserves_original_source() {
8840        let initial_source = "\
8841@settings(defaultLengthUnit = in, kclVersion = 2.0)
8842cylinder = startSketchOn(XY)
8843    |> circle(center= [0, 0], radius= 22)
8844    |> extrude(length = 14)
8845";
8846        let mut frontend = FrontendState::new();
8847        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8848        let outcome = ExecOutcome {
8849            variables: Default::default(),
8850            operations: Default::default(),
8851            artifact_graph: Default::default(),
8852            scene_objects: Default::default(),
8853            source_range_to_object: Default::default(),
8854            var_solutions: Default::default(),
8855            issues: Default::default(),
8856            filenames: Default::default(),
8857            default_planes: Default::default(),
8858        };
8859
8860        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8861
8862        assert_eq!(source_delta.text, initial_source);
8863    }
8864
8865    /// Explicit drag anchors should limit which edited points become temporary
8866    /// fixed constraints.
8867    #[tokio::test(flavor = "multi_thread")]
8868    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8869        let initial_source = "\
8870sketch(on = XY) {
8871  point1 = point(at = [var 0mm, var 0mm])
8872  point2 = point(at = [var 0mm, var 0mm])
8873  coincident([point1, point2])
8874}
8875";
8876
8877        let program = Program::parse(initial_source).unwrap().0.unwrap();
8878        let mut frontend = FrontendState::new();
8879        let mock_ctx = ExecutorContext::new_mock(None).await;
8880        let version = Version(0);
8881
8882        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8883        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8884        let sketch_id = sketch_object.id;
8885        let sketch = expect_sketch(sketch_object);
8886        let point1_id = sketch.segments[0];
8887        let point2_id = sketch.segments[1];
8888
8889        let segments = vec![
8890            ExistingSegmentCtor {
8891                id: point1_id,
8892                ctor: SegmentCtor::Point(PointCtor {
8893                    position: point_expr_mm(10.0, 0.0),
8894                }),
8895            },
8896            ExistingSegmentCtor {
8897                id: point2_id,
8898                ctor: SegmentCtor::Point(PointCtor {
8899                    position: point_expr_mm(100.0, 0.0),
8900                }),
8901            },
8902        ];
8903        let (_, scene_delta) = frontend
8904            .edit_segments_with_options(
8905                &mock_ctx,
8906                version,
8907                sketch_id,
8908                segments,
8909                EditSegmentsOptions {
8910                    anchor_segment_ids: Some(vec![point1_id]),
8911                    drag_anchors: Vec::new(),
8912                    commit_solved_initial_guesses: true,
8913                },
8914            )
8915            .await
8916            .unwrap();
8917
8918        assert_point_position_close(
8919            point_position(&scene_delta.new_graph, point1_id),
8920            point_number_mm(10.0, 0.0),
8921        );
8922        assert_point_position_close(
8923            point_position(&scene_delta.new_graph, point2_id),
8924            point_number_mm(10.0, 0.0),
8925        );
8926
8927        mock_ctx.close().await;
8928    }
8929
8930    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
8931    /// for every SketchVar whose initial NumericLiteral has the given value.
8932    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8933        use std::cell::RefCell;
8934        struct Collector {
8935            target: f64,
8936            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8937        }
8938        impl<'a> crate::walk::Visitor<'a> for &Collector {
8939            type Error = crate::front::Error;
8940            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8941                if let crate::walk::Node::SketchVar(sketch_var) = node
8942                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8943                    && (initial.value - self.target).abs() < 1e-9
8944                {
8945                    self.out
8946                        .borrow_mut()
8947                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8948                }
8949                for child in node.children().iter() {
8950                    if !child.visit(*self)? {
8951                        return Ok(false);
8952                    }
8953                }
8954                Ok(true)
8955            }
8956        }
8957        let collector = Collector {
8958            target: value,
8959            out: Default::default(),
8960        };
8961        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8962        collector.out.into_inner()
8963    }
8964
8965    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
8966    /// for every SketchVar (including bare `var`).
8967    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8968        use std::cell::RefCell;
8969        struct Collector {
8970            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8971        }
8972        impl<'a> crate::walk::Visitor<'a> for &Collector {
8973            type Error = crate::front::Error;
8974            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8975                if let crate::walk::Node::SketchVar(sketch_var) = node
8976                    && let Some(node_path) = &sketch_var.node_path
8977                {
8978                    self.out
8979                        .borrow_mut()
8980                        .push((SourceRange::from(sketch_var), node_path.clone()));
8981                }
8982                for child in node.children().iter() {
8983                    if !child.visit(*self)? {
8984                        return Ok(false);
8985                    }
8986                }
8987                Ok(true)
8988            }
8989        }
8990        let collector = Collector {
8991            out: Default::default(),
8992        };
8993        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8994        collector.out.into_inner()
8995    }
8996
8997    fn empty_exec_outcome_with_var_solutions(
8998        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8999    ) -> ExecOutcome {
9000        ExecOutcome {
9001            variables: Default::default(),
9002            operations: Default::default(),
9003            artifact_graph: Default::default(),
9004            scene_objects: Default::default(),
9005            source_range_to_object: Default::default(),
9006            var_solutions,
9007            issues: Default::default(),
9008            filenames: Default::default(),
9009            default_planes: Default::default(),
9010        }
9011    }
9012
9013    /// Happy path: commit a var solution to a `var N` inside a sketch block
9014    /// using a correct NodePath. Confirms the node-path code path produces the
9015    /// expected source mutation.
9016    #[test]
9017    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9018        let initial_source = "\
9019sketch(on = XY) {
9020  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9021}
9022";
9023        let program = Program::parse(initial_source).unwrap().0.unwrap();
9024        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9025        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9026        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9027
9028        let mut frontend = FrontendState::new();
9029        frontend.program = program;
9030
9031        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9032            literal_range,
9033            Some(node_path),
9034            Number {
9035                value: 25.0,
9036                units: NumericSuffix::Mm,
9037            },
9038        )]);
9039
9040        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9041
9042        assert_eq!(
9043            source_delta.text,
9044            "\
9045sketch(on = XY) {
9046  line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9047}
9048",
9049        );
9050    }
9051
9052    /// Whitespace inserted earlier in the source shifts the original SketchVar
9053    /// SourceRange. With NodePath propagation the commit should still target
9054    /// the right `var`. We simulate this by collecting node_paths against a
9055    /// "compact" source, then loading the frontend with a "padded" source
9056    /// (whose byte offsets differ), and feeding the original (now stale)
9057    /// source range plus the correct node_path back into the commit.
9058    #[test]
9059    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9060        let compact_source = "\
9061sketch(on = XY) {
9062  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9063}
9064";
9065        let padded_source = "\
9066// added comment\n// added comment\n\nsketch(on = XY) {
9067  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9068}
9069";
9070        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9071        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9072
9073        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9074            .into_iter()
9075            .next()
9076            .expect("expected `var 10mm` in compact source");
9077        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9078            .into_iter()
9079            .next()
9080            .expect("expected `var 10mm` in padded source");
9081
9082        assert_ne!(
9083            compact_match.0, padded_match.0,
9084            "byte offsets must differ for this test to be meaningful"
9085        );
9086        assert_eq!(
9087            compact_match.1, padded_match.1,
9088            "node paths must agree across whitespace; that's the whole point of NodePath",
9089        );
9090
9091        let mut frontend = FrontendState::new();
9092        frontend.program = padded_program;
9093
9094        // Stale source range from the compact source + correct node_path.
9095        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9096            compact_match.0,
9097            Some(compact_match.1),
9098            Number {
9099                value: 30.0,
9100                units: NumericSuffix::Mm,
9101            },
9102        )]);
9103
9104        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9105
9106        assert_eq!(
9107            source_delta.text,
9108            "\
9109// added comment
9110// added comment
9111
9112sketch(on = XY) {
9113  line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9114}
9115",
9116        );
9117    }
9118
9119    /// When multiple `var` declarations exist and the stale source range
9120    /// happens to land on a *different* var, the node_path must take
9121    /// precedence and the right var gets updated.
9122    #[test]
9123    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9124        let initial_source = "\
9125sketch(on = XY) {
9126  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9127}
9128";
9129        let program = Program::parse(initial_source).unwrap().0.unwrap();
9130
9131        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9132            .into_iter()
9133            .next()
9134            .expect("expected `var 10mm`");
9135        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9136            .into_iter()
9137            .next()
9138            .expect("expected `var 20mm`");
9139
9140        let mut frontend = FrontendState::new();
9141        frontend.program = program;
9142
9143        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9144        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9145            var_20.0,
9146            Some(var_10.1),
9147            Number {
9148                value: 33.0,
9149                units: NumericSuffix::Mm,
9150            },
9151        )]);
9152
9153        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9154
9155        assert_eq!(
9156            source_delta.text,
9157            "\
9158sketch(on = XY) {
9159  line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9160}
9161",
9162        );
9163    }
9164
9165    /// Bare `var` (no initial literal) is only locatable via node_path. With
9166    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9167    /// solver solution should fill the initial value in. The
9168    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9169    /// is gated as an experimental feature; without it the re-parse of the
9170    /// recast source rejects bare `var` declarations.
9171    #[test]
9172    fn test_commit_var_solution_writes_back_into_bare_var() {
9173        let initial_source = "\
9174@settings(experimentalFeatures = allow, kclVersion = 2.0)
9175sketch(on = XY) {
9176  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9177}
9178";
9179        let program = Program::parse(initial_source).unwrap().0.unwrap();
9180
9181        // Pick the first bare `var`; collect_all_sketch_vars returns every
9182        // SketchVar, including bare ones.
9183        let bare = collect_all_sketch_vars(&program)
9184            .into_iter()
9185            .find(|(range, _)| {
9186                // The bare `var` is exactly the 3 characters "var".
9187                range.end() - range.start() == 3
9188            })
9189            .expect("expected at least one bare `var`");
9190
9191        let mut frontend = FrontendState::new();
9192        frontend.program = program;
9193
9194        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9195            bare.0,
9196            Some(bare.1),
9197            Number {
9198                value: 7.0,
9199                units: NumericSuffix::Mm,
9200            },
9201        )]);
9202
9203        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9204
9205        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9206        // written as an explicit suffix so the bare var commits with units.
9207        // The recast adds a blank line after the `@settings` annotation.
9208        assert_eq!(
9209            source_delta.text,
9210            "\
9211@settings(experimentalFeatures = allow, kclVersion = 2.0)
9212
9213sketch(on = XY) {
9214  line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9215}
9216",
9217        );
9218    }
9219
9220    #[tokio::test(flavor = "multi_thread")]
9221    async fn test_delete_point_without_var() {
9222        let initial_source = "\
9223sketch(on = XY) {
9224  point(at = [var 1, var 2])
9225  point(at = [var 3, var 4])
9226  point(at = [var 5, var 6])
9227}
9228";
9229
9230        let program = Program::parse(initial_source).unwrap().0.unwrap();
9231
9232        let mut frontend = FrontendState::new();
9233
9234        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9235        let mock_ctx = ExecutorContext::new_mock(None).await;
9236        let version = Version(0);
9237
9238        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9239        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9240        let sketch_id = sketch_object.id;
9241        let sketch = expect_sketch(sketch_object);
9242
9243        let point_id = *sketch.segments.get(1).unwrap();
9244
9245        let (src_delta, scene_delta) = frontend
9246            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9247            .await
9248            .unwrap();
9249        assert_eq!(
9250            src_delta.text.as_str(),
9251            "\
9252sketch(on = XY) {
9253  point(at = [var 1, var 2])
9254  point(at = [var 5, var 6])
9255}
9256"
9257        );
9258        assert_eq!(scene_delta.new_objects, vec![]);
9259        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9260
9261        ctx.close().await;
9262        mock_ctx.close().await;
9263    }
9264
9265    #[tokio::test(flavor = "multi_thread")]
9266    async fn test_delete_point_with_var() {
9267        let initial_source = "\
9268sketch(on = XY) {
9269  point(at = [var 1, var 2])
9270  point1 = point(at = [var 3, var 4])
9271  point(at = [var 5, var 6])
9272}
9273";
9274
9275        let program = Program::parse(initial_source).unwrap().0.unwrap();
9276
9277        let mut frontend = FrontendState::new();
9278
9279        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9280        let mock_ctx = ExecutorContext::new_mock(None).await;
9281        let version = Version(0);
9282
9283        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9284        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9285        let sketch_id = sketch_object.id;
9286        let sketch = expect_sketch(sketch_object);
9287
9288        let point_id = *sketch.segments.get(1).unwrap();
9289
9290        let (src_delta, scene_delta) = frontend
9291            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9292            .await
9293            .unwrap();
9294        assert_eq!(
9295            src_delta.text.as_str(),
9296            "\
9297sketch(on = XY) {
9298  point(at = [var 1, var 2])
9299  point(at = [var 5, var 6])
9300}
9301"
9302        );
9303        assert_eq!(scene_delta.new_objects, vec![]);
9304        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9305
9306        ctx.close().await;
9307        mock_ctx.close().await;
9308    }
9309
9310    #[tokio::test(flavor = "multi_thread")]
9311    async fn test_delete_multiple_points() {
9312        let initial_source = "\
9313sketch(on = XY) {
9314  point(at = [var 1, var 2])
9315  point1 = point(at = [var 3, var 4])
9316  point(at = [var 5, var 6])
9317}
9318";
9319
9320        let program = Program::parse(initial_source).unwrap().0.unwrap();
9321
9322        let mut frontend = FrontendState::new();
9323
9324        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9325        let mock_ctx = ExecutorContext::new_mock(None).await;
9326        let version = Version(0);
9327
9328        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9329        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9330        let sketch_id = sketch_object.id;
9331
9332        let sketch = expect_sketch(sketch_object);
9333
9334        let point1_id = *sketch.segments.first().unwrap();
9335        let point2_id = *sketch.segments.get(1).unwrap();
9336
9337        let (src_delta, scene_delta) = frontend
9338            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9339            .await
9340            .unwrap();
9341        assert_eq!(
9342            src_delta.text.as_str(),
9343            "\
9344sketch(on = XY) {
9345  point(at = [var 5, var 6])
9346}
9347"
9348        );
9349        assert_eq!(scene_delta.new_objects, vec![]);
9350        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9351
9352        ctx.close().await;
9353        mock_ctx.close().await;
9354    }
9355
9356    #[tokio::test(flavor = "multi_thread")]
9357    async fn test_delete_coincident_constraint() {
9358        let initial_source = "\
9359sketch(on = XY) {
9360  point1 = point(at = [var 1, var 2])
9361  point2 = point(at = [var 3, var 4])
9362  coincident([point1, point2])
9363  point(at = [var 5, var 6])
9364}
9365";
9366
9367        let program = Program::parse(initial_source).unwrap().0.unwrap();
9368
9369        let mut frontend = FrontendState::new();
9370
9371        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9372        let mock_ctx = ExecutorContext::new_mock(None).await;
9373        let version = Version(0);
9374
9375        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9376        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9377        let sketch_id = sketch_object.id;
9378        let sketch = expect_sketch(sketch_object);
9379
9380        let coincident_id = *sketch.constraints.first().unwrap();
9381
9382        let (src_delta, scene_delta) = frontend
9383            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9384            .await
9385            .unwrap();
9386        assert_eq!(
9387            src_delta.text.as_str(),
9388            "\
9389sketch(on = XY) {
9390  point1 = point(at = [var 1, var 2])
9391  point2 = point(at = [var 3, var 4])
9392  point(at = [var 5, var 6])
9393}
9394"
9395        );
9396        assert_eq!(scene_delta.new_objects, vec![]);
9397        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9398
9399        ctx.close().await;
9400        mock_ctx.close().await;
9401    }
9402
9403    #[tokio::test(flavor = "multi_thread")]
9404    async fn test_delete_line_cascades_to_coincident_constraint() {
9405        let initial_source = "\
9406sketch(on = XY) {
9407  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9408  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9409  coincident([line1.end, line2.start])
9410}
9411";
9412
9413        let program = Program::parse(initial_source).unwrap().0.unwrap();
9414
9415        let mut frontend = FrontendState::new();
9416
9417        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9418        let mock_ctx = ExecutorContext::new_mock(None).await;
9419        let version = Version(0);
9420
9421        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9422        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9423        let sketch_id = sketch_object.id;
9424        let sketch = expect_sketch(sketch_object);
9425        let line_id = *sketch.segments.get(5).unwrap();
9426
9427        let (src_delta, scene_delta) = frontend
9428            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9429            .await
9430            .unwrap();
9431        assert_eq!(
9432            src_delta.text.as_str(),
9433            "\
9434sketch(on = XY) {
9435  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9436}
9437"
9438        );
9439        assert_eq!(
9440            scene_delta.new_graph.objects.len(),
9441            5,
9442            "{:#?}",
9443            scene_delta.new_graph.objects
9444        );
9445
9446        ctx.close().await;
9447        mock_ctx.close().await;
9448    }
9449
9450    #[tokio::test(flavor = "multi_thread")]
9451    async fn test_delete_line_cascades_to_distance_constraint() {
9452        let initial_source = "\
9453sketch(on = XY) {
9454  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9455  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9456  distance([line1.end, line2.start]) == 10mm
9457}
9458";
9459
9460        let program = Program::parse(initial_source).unwrap().0.unwrap();
9461
9462        let mut frontend = FrontendState::new();
9463
9464        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9465        let mock_ctx = ExecutorContext::new_mock(None).await;
9466        let version = Version(0);
9467
9468        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9469        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9470        let sketch_id = sketch_object.id;
9471        let sketch = expect_sketch(sketch_object);
9472        let line_id = *sketch.segments.get(5).unwrap();
9473
9474        let (src_delta, scene_delta) = frontend
9475            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9476            .await
9477            .unwrap();
9478        assert_eq!(
9479            src_delta.text.as_str(),
9480            "\
9481sketch(on = XY) {
9482  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9483}
9484"
9485        );
9486        assert_eq!(
9487            scene_delta.new_graph.objects.len(),
9488            5,
9489            "{:#?}",
9490            scene_delta.new_graph.objects
9491        );
9492
9493        ctx.close().await;
9494        mock_ctx.close().await;
9495    }
9496
9497    #[tokio::test(flavor = "multi_thread")]
9498    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9499        let initial_source = "\
9500sketch(on = XY) {
9501  point1 = point(at = [var 1, var 2])
9502  point2 = point(at = [var 3, var 4])
9503  horizontalDistance([point1, point2]) == 10mm
9504}
9505";
9506
9507        let program = Program::parse(initial_source).unwrap().0.unwrap();
9508
9509        let mut frontend = FrontendState::new();
9510
9511        let mock_ctx = ExecutorContext::new_mock(None).await;
9512        let version = Version(0);
9513
9514        frontend.program = program.clone();
9515        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9516        frontend.update_state_after_exec(outcome, true);
9517        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9518        let sketch_id = sketch_object.id;
9519        let sketch = expect_sketch(sketch_object);
9520        let point2_id = *sketch.segments.get(1).unwrap();
9521
9522        let (src_delta, scene_delta) = frontend
9523            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9524            .await
9525            .unwrap();
9526        assert_eq!(
9527            src_delta.text.as_str(),
9528            "\
9529sketch(on = XY) {
9530  point1 = point(at = [var 1, var 2])
9531}
9532"
9533        );
9534        assert_eq!(
9535            scene_delta.new_graph.objects.len(),
9536            3,
9537            "{:#?}",
9538            scene_delta.new_graph.objects
9539        );
9540
9541        mock_ctx.close().await;
9542    }
9543
9544    #[tokio::test(flavor = "multi_thread")]
9545    async fn test_delete_line_cascades_to_fixed_constraint() {
9546        let initial_source = "\
9547sketch(on = XY) {
9548  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9549  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9550  fixed([line1.start, [0, 0]])
9551}
9552";
9553
9554        let program = Program::parse(initial_source).unwrap().0.unwrap();
9555
9556        let mut frontend = FrontendState::new();
9557
9558        let mock_ctx = ExecutorContext::new_mock(None).await;
9559        let version = Version(0);
9560
9561        frontend.program = program.clone();
9562        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9563        frontend.update_state_after_exec(outcome, true);
9564        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9565        let sketch_id = sketch_object.id;
9566        let sketch = expect_sketch(sketch_object);
9567        let line1_id = *sketch.segments.get(2).unwrap();
9568
9569        let (src_delta, scene_delta) = frontend
9570            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9571            .await
9572            .unwrap();
9573        assert_eq!(
9574            src_delta.text.as_str(),
9575            "\
9576sketch(on = XY) {
9577  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9578}
9579"
9580        );
9581        assert_eq!(
9582            scene_delta.new_graph.objects.len(),
9583            5,
9584            "{:#?}",
9585            scene_delta.new_graph.objects
9586        );
9587
9588        mock_ctx.close().await;
9589    }
9590
9591    #[tokio::test(flavor = "multi_thread")]
9592    async fn test_delete_line_cascades_to_midpoint_constraint() {
9593        let initial_source = "\
9594sketch(on = XY) {
9595  point1 = point(at = [var 1, var 2])
9596  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9597  midpoint(line1, point = point1)
9598}
9599";
9600
9601        let program = Program::parse(initial_source).unwrap().0.unwrap();
9602
9603        let mut frontend = FrontendState::new();
9604
9605        let mock_ctx = ExecutorContext::new_mock(None).await;
9606        let version = Version(0);
9607
9608        frontend.program = program.clone();
9609        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9610        frontend.update_state_after_exec(outcome, true);
9611        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9612        let sketch_id = sketch_object.id;
9613        let sketch = expect_sketch(sketch_object);
9614        let line1_id = *sketch.segments.get(3).unwrap();
9615
9616        let (src_delta, scene_delta) = frontend
9617            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9618            .await
9619            .unwrap();
9620        assert_eq!(
9621            src_delta.text.as_str(),
9622            "\
9623sketch(on = XY) {
9624  point1 = point(at = [var 1, var 2])
9625}
9626"
9627        );
9628        assert_eq!(
9629            scene_delta.new_graph.objects.len(),
9630            3,
9631            "{:#?}",
9632            scene_delta.new_graph.objects
9633        );
9634
9635        mock_ctx.close().await;
9636    }
9637
9638    #[tokio::test(flavor = "multi_thread")]
9639    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9640        let initial_source = "\
9641sketch(on = XY) {
9642  point1 = point(at = [var 1, var 2])
9643  point2 = point(at = [var 3, var 4])
9644  point3 = point(at = [var 5, var 6])
9645  coincident([point1, point2, point3])
9646}
9647";
9648
9649        let program = Program::parse(initial_source).unwrap().0.unwrap();
9650
9651        let mut frontend = FrontendState::new();
9652
9653        let mock_ctx = ExecutorContext::new_mock(None).await;
9654        let version = Version(0);
9655
9656        frontend.program = program.clone();
9657        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9658        frontend.update_state_after_exec(outcome, true);
9659        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9660        let sketch_id = sketch_object.id;
9661        let sketch = expect_sketch(sketch_object);
9662        let point3_id = *sketch.segments.get(2).unwrap();
9663
9664        let (src_delta, scene_delta) = frontend
9665            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9666            .await
9667            .unwrap();
9668        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9669        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9670        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9671        assert!(
9672            src_delta.text.contains("coincident([point1, point2])"),
9673            "{}",
9674            src_delta.text
9675        );
9676
9677        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9678        let sketch = expect_sketch(sketch_object);
9679        assert_eq!(sketch.segments.len(), 2);
9680        assert_eq!(sketch.constraints.len(), 1);
9681
9682        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9683        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9684            panic!("Expected constraint object");
9685        };
9686        let Constraint::Coincident(coincident) = constraint else {
9687            panic!("Expected coincident constraint");
9688        };
9689        assert_eq!(
9690            coincident.segments,
9691            sketch
9692                .segments
9693                .iter()
9694                .copied()
9695                .map(Into::into)
9696                .collect::<Vec<ConstraintSegment>>()
9697        );
9698
9699        mock_ctx.close().await;
9700    }
9701
9702    #[tokio::test(flavor = "multi_thread")]
9703    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9704        let initial_source = "\
9705sketch(on = XY) {
9706  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9707  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9708  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9709  equalLength([line1, line2, line3])
9710}
9711";
9712
9713        let program = Program::parse(initial_source).unwrap().0.unwrap();
9714
9715        let mut frontend = FrontendState::new();
9716
9717        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9718        let mock_ctx = ExecutorContext::new_mock(None).await;
9719        let version = Version(0);
9720
9721        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9722        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9723        let sketch_id = sketch_object.id;
9724        let sketch = expect_sketch(sketch_object);
9725        let line3_id = *sketch.segments.get(8).unwrap();
9726
9727        let (src_delta, scene_delta) = frontend
9728            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9729            .await
9730            .unwrap();
9731        assert_eq!(
9732            src_delta.text.as_str(),
9733            "\
9734sketch(on = XY) {
9735  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9736  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9737  equalLength([line1, line2])
9738}
9739"
9740        );
9741
9742        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9743        let sketch = expect_sketch(sketch_object);
9744        assert_eq!(sketch.constraints.len(), 1);
9745
9746        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9747        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9748            panic!("Expected constraint object");
9749        };
9750        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9751            panic!("Expected lines equal length constraint");
9752        };
9753        assert_eq!(lines_equal_length.lines.len(), 2);
9754
9755        ctx.close().await;
9756        mock_ctx.close().await;
9757    }
9758
9759    #[tokio::test(flavor = "multi_thread")]
9760    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9761        let initial_source = "\
9762sketch(on = XY) {
9763  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9764  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9765  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9766  horizontal([line1.end, line2.start, line3.start])
9767}
9768";
9769
9770        let program = Program::parse(initial_source).unwrap().0.unwrap();
9771
9772        let mut frontend = FrontendState::new();
9773
9774        let mock_ctx = ExecutorContext::new_mock(None).await;
9775        let version = Version(0);
9776
9777        frontend.program = program.clone();
9778        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9779        frontend.update_state_after_exec(outcome, true);
9780        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9781        let sketch_id = sketch_object.id;
9782        let sketch = expect_sketch(sketch_object);
9783        let line1_id = *sketch.segments.get(2).unwrap();
9784
9785        let (src_delta, scene_delta) = frontend
9786            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9787            .await
9788            .unwrap();
9789        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9790        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9791        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9792        assert!(
9793            src_delta.text.contains("horizontal([line2.start, line3.start])"),
9794            "{}",
9795            src_delta.text
9796        );
9797
9798        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9799        let sketch = expect_sketch(sketch_object);
9800        assert_eq!(sketch.constraints.len(), 1);
9801
9802        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9803        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9804            panic!("Expected constraint object");
9805        };
9806        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9807            panic!("Expected horizontal points constraint");
9808        };
9809        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9810        assert_eq!(*points, remaining_points);
9811
9812        mock_ctx.close().await;
9813    }
9814
9815    #[tokio::test(flavor = "multi_thread")]
9816    async fn test_delete_line_preserves_multiline_vertical_constraint() {
9817        let initial_source = "\
9818sketch(on = XY) {
9819  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9821  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9822  vertical([line1.end, line2.start, line3.start])
9823}
9824";
9825
9826        let program = Program::parse(initial_source).unwrap().0.unwrap();
9827
9828        let mut frontend = FrontendState::new();
9829
9830        let mock_ctx = ExecutorContext::new_mock(None).await;
9831        let version = Version(0);
9832
9833        frontend.program = program.clone();
9834        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9835        frontend.update_state_after_exec(outcome, true);
9836        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837        let sketch_id = sketch_object.id;
9838        let sketch = expect_sketch(sketch_object);
9839        let line1_id = *sketch.segments.get(2).unwrap();
9840
9841        let (src_delta, scene_delta) = frontend
9842            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9843            .await
9844            .unwrap();
9845        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9846        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9847        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9848        assert!(
9849            src_delta.text.contains("vertical([line2.start, line3.start])"),
9850            "{}",
9851            src_delta.text
9852        );
9853
9854        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9855        let sketch = expect_sketch(sketch_object);
9856        assert_eq!(sketch.constraints.len(), 1);
9857
9858        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9859        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9860            panic!("Expected constraint object");
9861        };
9862        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9863            panic!("Expected vertical points constraint");
9864        };
9865        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9866        assert_eq!(*points, remaining_points);
9867
9868        mock_ctx.close().await;
9869    }
9870
9871    #[tokio::test(flavor = "multi_thread")]
9872    async fn test_delete_line_preserves_multiline_coincident_constraint() {
9873        let initial_source = "\
9874sketch(on = XY) {
9875  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9876  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9877  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9878  coincident([line1.end, line2.start, line3.start])
9879}
9880";
9881
9882        let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884        let mut frontend = FrontendState::new();
9885
9886        let mock_ctx = ExecutorContext::new_mock(None).await;
9887        let version = Version(0);
9888
9889        frontend.program = program.clone();
9890        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9891        frontend.update_state_after_exec(outcome, true);
9892        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9893        let sketch_id = sketch_object.id;
9894        let sketch = expect_sketch(sketch_object);
9895        let line1_id = *sketch.segments.get(2).unwrap();
9896
9897        let (src_delta, scene_delta) = frontend
9898            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9899            .await
9900            .unwrap();
9901        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9902        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9903        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9904        assert!(
9905            src_delta.text.contains("coincident([line2.start, line3.start])"),
9906            "{}",
9907            src_delta.text
9908        );
9909
9910        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9911        let sketch = expect_sketch(sketch_object);
9912        assert_eq!(sketch.constraints.len(), 1);
9913
9914        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9915        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9916            panic!("Expected constraint object");
9917        };
9918        let Constraint::Coincident(coincident) = constraint else {
9919            panic!("Expected coincident constraint");
9920        };
9921        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9922        assert_eq!(coincident.segments, remaining_segments);
9923
9924        mock_ctx.close().await;
9925    }
9926
9927    #[tokio::test(flavor = "multi_thread")]
9928    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9929        let initial_source = "\
9930sketch(on = XY) {
9931  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9932  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9933  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9934  equalLength([line1, line2, line3])
9935}
9936";
9937
9938        let program = Program::parse(initial_source).unwrap().0.unwrap();
9939
9940        let mut frontend = FrontendState::new();
9941
9942        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9943        let mock_ctx = ExecutorContext::new_mock(None).await;
9944        let version = Version(0);
9945
9946        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9947        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9948        let sketch_id = sketch_object.id;
9949        let sketch = expect_sketch(sketch_object);
9950        let line2_id = *sketch.segments.get(5).unwrap();
9951        let line3_id = *sketch.segments.get(8).unwrap();
9952
9953        let (src_delta, scene_delta) = frontend
9954            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9955            .await
9956            .unwrap();
9957        assert_eq!(
9958            src_delta.text.as_str(),
9959            "\
9960sketch(on = XY) {
9961  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9962}
9963"
9964        );
9965
9966        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9967        let sketch = expect_sketch(sketch_object);
9968        assert!(sketch.constraints.is_empty());
9969
9970        ctx.close().await;
9971        mock_ctx.close().await;
9972    }
9973
9974    #[tokio::test(flavor = "multi_thread")]
9975    async fn test_delete_line_preserves_multiline_parallel_constraint() {
9976        let initial_source = "\
9977sketch(on = XY) {
9978  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9979  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9980  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9981  parallel([line1, line2, line3])
9982}
9983";
9984
9985        let program = Program::parse(initial_source).unwrap().0.unwrap();
9986
9987        let mut frontend = FrontendState::new();
9988
9989        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9990        let mock_ctx = ExecutorContext::new_mock(None).await;
9991        let version = Version(0);
9992
9993        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9994        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9995        let sketch_id = sketch_object.id;
9996        let sketch = expect_sketch(sketch_object);
9997        let line3_id = *sketch.segments.get(8).unwrap();
9998
9999        let (src_delta, scene_delta) = frontend
10000            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10001            .await
10002            .unwrap();
10003        assert_eq!(
10004            src_delta.text.as_str(),
10005            "\
10006sketch(on = XY) {
10007  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10008  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10009  parallel([line1, line2])
10010}
10011"
10012        );
10013
10014        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10015        let sketch = expect_sketch(sketch_object);
10016        assert_eq!(sketch.constraints.len(), 1);
10017
10018        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10019        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10020            panic!("Expected constraint object");
10021        };
10022        let Constraint::Parallel(parallel) = constraint else {
10023            panic!("Expected parallel constraint");
10024        };
10025        assert_eq!(parallel.lines.len(), 2);
10026
10027        ctx.close().await;
10028        mock_ctx.close().await;
10029    }
10030
10031    #[tokio::test(flavor = "multi_thread")]
10032    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10033        let initial_source = "\
10034sketch(on = XY) {
10035  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10036  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10037  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10038  parallel([line1, line2, line3])
10039}
10040";
10041
10042        let program = Program::parse(initial_source).unwrap().0.unwrap();
10043
10044        let mut frontend = FrontendState::new();
10045
10046        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10047        let mock_ctx = ExecutorContext::new_mock(None).await;
10048        let version = Version(0);
10049
10050        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10051        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10052        let sketch_id = sketch_object.id;
10053        let sketch = expect_sketch(sketch_object);
10054        let line2_id = *sketch.segments.get(5).unwrap();
10055        let line3_id = *sketch.segments.get(8).unwrap();
10056
10057        let (src_delta, scene_delta) = frontend
10058            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10059            .await
10060            .unwrap();
10061        assert_eq!(
10062            src_delta.text.as_str(),
10063            "\
10064sketch(on = XY) {
10065  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10066}
10067"
10068        );
10069
10070        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10071        let sketch = expect_sketch(sketch_object);
10072        assert!(sketch.constraints.is_empty());
10073
10074        ctx.close().await;
10075        mock_ctx.close().await;
10076    }
10077
10078    #[tokio::test(flavor = "multi_thread")]
10079    async fn test_delete_line_line_coincident_constraint() {
10080        let initial_source = "\
10081sketch(on = XY) {
10082  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10083  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10084  coincident([line1, line2])
10085}
10086";
10087
10088        let program = Program::parse(initial_source).unwrap().0.unwrap();
10089
10090        let mut frontend = FrontendState::new();
10091
10092        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10093        let mock_ctx = ExecutorContext::new_mock(None).await;
10094        let version = Version(0);
10095
10096        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10097        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10098        let sketch_id = sketch_object.id;
10099        let sketch = expect_sketch(sketch_object);
10100
10101        let coincident_id = *sketch.constraints.first().unwrap();
10102
10103        let (src_delta, scene_delta) = frontend
10104            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10105            .await
10106            .unwrap();
10107        assert_eq!(
10108            src_delta.text.as_str(),
10109            "\
10110sketch(on = XY) {
10111  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10112  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10113}
10114"
10115        );
10116        assert_eq!(scene_delta.new_objects, vec![]);
10117        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10118
10119        ctx.close().await;
10120        mock_ctx.close().await;
10121    }
10122
10123    #[tokio::test(flavor = "multi_thread")]
10124    async fn test_two_points_coincident() {
10125        let initial_source = "\
10126sketch(on = XY) {
10127  point1 = point(at = [var 1, var 2])
10128  point(at = [3, 4])
10129}
10130";
10131
10132        let program = Program::parse(initial_source).unwrap().0.unwrap();
10133
10134        let mut frontend = FrontendState::new();
10135
10136        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10137        let mock_ctx = ExecutorContext::new_mock(None).await;
10138        let version = Version(0);
10139
10140        frontend.hack_set_program(&ctx, program).await.unwrap();
10141        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10142        let sketch_id = sketch_object.id;
10143        let sketch = expect_sketch(sketch_object);
10144        let point0_id = *sketch.segments.first().unwrap();
10145        let point1_id = *sketch.segments.get(1).unwrap();
10146
10147        let constraint = Constraint::Coincident(Coincident {
10148            segments: vec![point0_id.into(), point1_id.into()],
10149        });
10150        let (src_delta, scene_delta) = frontend
10151            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10152            .await
10153            .unwrap();
10154        assert_eq!(
10155            src_delta.text.as_str(),
10156            "\
10157sketch(on = XY) {
10158  point1 = point(at = [var 3, var 4])
10159  point2 = point(at = [3, 4])
10160  coincident([point1, point2])
10161}
10162"
10163        );
10164        assert_eq!(
10165            scene_delta.new_graph.objects.len(),
10166            5,
10167            "{:#?}",
10168            scene_delta.new_graph.objects
10169        );
10170
10171        ctx.close().await;
10172        mock_ctx.close().await;
10173    }
10174
10175    #[tokio::test(flavor = "multi_thread")]
10176    async fn test_three_points_coincident() {
10177        let initial_source = "\
10178sketch(on = XY) {
10179  point1 = point(at = [var 1, var 2])
10180  point(at = [var 3, var 4])
10181  point(at = [var 5, var 6])
10182}
10183";
10184
10185        let program = Program::parse(initial_source).unwrap().0.unwrap();
10186
10187        let mut frontend = FrontendState::new();
10188
10189        let mock_ctx = ExecutorContext::new_mock(None).await;
10190        let version = Version(0);
10191
10192        frontend.program = program.clone();
10193        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10194        frontend.update_state_after_exec(outcome, true);
10195        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10196        let sketch_id = sketch_object.id;
10197        let sketch = expect_sketch(sketch_object);
10198        let segments = sketch
10199            .segments
10200            .iter()
10201            .take(3)
10202            .copied()
10203            .map(Into::into)
10204            .collect::<Vec<ConstraintSegment>>();
10205
10206        let constraint = Constraint::Coincident(Coincident {
10207            segments: segments.clone(),
10208        });
10209        let (src_delta, scene_delta) = frontend
10210            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10211            .await
10212            .unwrap();
10213        assert_eq!(
10214            src_delta.text.as_str(),
10215            "\
10216sketch(on = XY) {
10217  point1 = point(at = [var 3, var 4])
10218  point2 = point(at = [var 3, var 4])
10219  point3 = point(at = [var 3, var 4])
10220  coincident([point1, point2, point3])
10221}
10222"
10223        );
10224
10225        let constraint_object = scene_delta
10226            .new_graph
10227            .objects
10228            .iter()
10229            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10230            .unwrap();
10231
10232        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10233            panic!("expected a constraint object");
10234        };
10235
10236        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10237
10238        mock_ctx.close().await;
10239    }
10240
10241    #[tokio::test(flavor = "multi_thread")]
10242    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10243        let initial_source = "\
10244sketch(on = XY) {
10245  point1 = point(at = [var 1, var 2])
10246  point2 = point(at = [var 3, var 4])
10247  point3 = point(at = [var 5, var 6])
10248  coincident([point1, point2, point3])
10249}
10250";
10251
10252        let program = Program::parse(initial_source).unwrap().0.unwrap();
10253
10254        let mut frontend = FrontendState::new();
10255
10256        let ctx = ExecutorContext::new_mock(None).await;
10257        frontend.program = program.clone();
10258        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10259        frontend.update_state_after_exec(outcome, true);
10260
10261        let constraint_object = frontend
10262            .scene_graph
10263            .objects
10264            .iter()
10265            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10266            .unwrap();
10267        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10268            panic!("expected a constraint object");
10269        };
10270
10271        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10272        let sketch = expect_sketch(sketch_object);
10273        let expected_segments = sketch
10274            .segments
10275            .iter()
10276            .take(3)
10277            .copied()
10278            .map(Into::into)
10279            .collect::<Vec<ConstraintSegment>>();
10280
10281        assert_eq!(
10282            constraint,
10283            &Constraint::Coincident(Coincident {
10284                segments: expected_segments,
10285            })
10286        );
10287
10288        ctx.close().await;
10289    }
10290
10291    #[tokio::test(flavor = "multi_thread")]
10292    async fn test_point_origin_coincident_preserves_order() {
10293        let initial_source = "\
10294sketch(on = XY) {
10295  point(at = [var 1, var 2])
10296}
10297";
10298
10299        for (origin_first, expected_source) in [
10300            (
10301                true,
10302                "\
10303sketch(on = XY) {
10304  point1 = point(at = [var 0, var 0])
10305  coincident([ORIGIN, point1])
10306}
10307",
10308            ),
10309            (
10310                false,
10311                "\
10312sketch(on = XY) {
10313  point1 = point(at = [var 0, var 0])
10314  coincident([point1, ORIGIN])
10315}
10316",
10317            ),
10318        ] {
10319            let program = Program::parse(initial_source).unwrap().0.unwrap();
10320
10321            let mut frontend = FrontendState::new();
10322
10323            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10324            let mock_ctx = ExecutorContext::new_mock(None).await;
10325            let version = Version(0);
10326
10327            frontend.hack_set_program(&ctx, program).await.unwrap();
10328            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10329            let sketch_id = sketch_object.id;
10330            let sketch = expect_sketch(sketch_object);
10331            let point_id = *sketch.segments.first().unwrap();
10332
10333            let segments = if origin_first {
10334                vec![ConstraintSegment::ORIGIN, point_id.into()]
10335            } else {
10336                vec![point_id.into(), ConstraintSegment::ORIGIN]
10337            };
10338            let constraint = Constraint::Coincident(Coincident {
10339                segments: segments.clone(),
10340            });
10341            let (src_delta, scene_delta) = frontend
10342                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10343                .await
10344                .unwrap();
10345            assert_eq!(src_delta.text.as_str(), expected_source);
10346
10347            let constraint_object = scene_delta
10348                .new_graph
10349                .objects
10350                .iter()
10351                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10352                .unwrap();
10353
10354            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10355                panic!("expected a constraint object");
10356            };
10357
10358            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10359
10360            ctx.close().await;
10361            mock_ctx.close().await;
10362        }
10363    }
10364
10365    #[tokio::test(flavor = "multi_thread")]
10366    async fn test_coincident_of_line_end_points() {
10367        let initial_source = "\
10368sketch(on = XY) {
10369  line(start = [var 1, var 2], end = [var 3, var 4])
10370  line(start = [var 5, var 6], end = [var 7, var 8])
10371}
10372";
10373
10374        let program = Program::parse(initial_source).unwrap().0.unwrap();
10375
10376        let mut frontend = FrontendState::new();
10377
10378        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10379        let mock_ctx = ExecutorContext::new_mock(None).await;
10380        let version = Version(0);
10381
10382        frontend.hack_set_program(&ctx, program).await.unwrap();
10383        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10384        let sketch_id = sketch_object.id;
10385        let sketch = expect_sketch(sketch_object);
10386        let point0_id = *sketch.segments.get(1).unwrap();
10387        let point1_id = *sketch.segments.get(3).unwrap();
10388
10389        let constraint = Constraint::Coincident(Coincident {
10390            segments: vec![point0_id.into(), point1_id.into()],
10391        });
10392        let (src_delta, scene_delta) = frontend
10393            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10394            .await
10395            .unwrap();
10396        assert_eq!(
10397            src_delta.text.as_str(),
10398            "\
10399sketch(on = XY) {
10400  line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10401  line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10402  coincident([line1.end, line2.start])
10403}
10404"
10405        );
10406        assert_eq!(
10407            scene_delta.new_graph.objects.len(),
10408            9,
10409            "{:#?}",
10410            scene_delta.new_graph.objects
10411        );
10412
10413        ctx.close().await;
10414        mock_ctx.close().await;
10415    }
10416
10417    #[tokio::test(flavor = "multi_thread")]
10418    async fn test_coincident_of_line_point_and_circle_segment() {
10419        let initial_source = "\
10420sketch(on = XY) {
10421  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10422  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10423}
10424";
10425        let program = Program::parse(initial_source).unwrap().0.unwrap();
10426        let mut frontend = FrontendState::new();
10427
10428        let mock_ctx = ExecutorContext::new_mock(None).await;
10429        let version = Version(0);
10430
10431        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10432        frontend.program = program;
10433        frontend.update_state_after_exec(outcome, true);
10434        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10435        let sketch_id = sketch_object.id;
10436        let sketch = expect_sketch(sketch_object);
10437
10438        let circle_id = sketch
10439            .segments
10440            .iter()
10441            .copied()
10442            .find(|seg_id| {
10443                matches!(
10444                    &frontend.scene_graph.objects[seg_id.0].kind,
10445                    ObjectKind::Segment {
10446                        segment: Segment::Circle(_)
10447                    }
10448                )
10449            })
10450            .expect("Expected a circle segment in sketch");
10451        let line_id = frontend
10452            .scene_graph
10453            .objects
10454            .iter()
10455            .find_map(|obj| match &obj.kind {
10456                ObjectKind::Segment {
10457                    segment: Segment::Line(line),
10458                } if line.owner.is_none() => Some(obj.id),
10459                _ => None,
10460            })
10461            .expect("Expected a standalone line segment in scene graph");
10462
10463        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10464            ObjectKind::Segment {
10465                segment: Segment::Line(line),
10466            } => line.start,
10467            _ => panic!("Expected line segment object"),
10468        };
10469
10470        let constraint = Constraint::Coincident(Coincident {
10471            segments: vec![line_start_point_id.into(), circle_id.into()],
10472        });
10473        let (src_delta, _scene_delta) = frontend
10474            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10475            .await
10476            .unwrap();
10477        assert_eq!(
10478            src_delta.text.as_str(),
10479            "\
10480sketch(on = XY) {
10481  circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10482  line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10483  coincident([line1.start, circle1])
10484}
10485"
10486        );
10487
10488        mock_ctx.close().await;
10489    }
10490
10491    #[tokio::test(flavor = "multi_thread")]
10492    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10493        // Test that attempting an invalid coincident constraint (arc and line)
10494        // doesn't corrupt the state, allowing subsequent operations to work.
10495        // This test verifies the transactional fix in add_constraint that prevents
10496        // state corruption when invalid constraints are attempted.
10497        // Example: coincident constraint between an arc segment and a straight line segment
10498        // is geometrically invalid and should fail, but state should remain intact.
10499        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10500        let program = Program::empty();
10501
10502        let mut frontend = FrontendState::new();
10503        frontend.program = program;
10504
10505        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10506        let mock_ctx = ExecutorContext::new_mock(None).await;
10507        let version = Version(0);
10508
10509        let sketch_args = SketchCtor {
10510            on: Plane::Default(PlaneName::Xy),
10511        };
10512        let (_src_delta, _scene_delta, sketch_id) = frontend
10513            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10514            .await
10515            .unwrap();
10516
10517        // Add an arc segment
10518        let arc_ctor = ArcCtor {
10519            start: Point2d {
10520                x: Expr::Var(Number {
10521                    value: 0.0,
10522                    units: NumericSuffix::Mm,
10523                }),
10524                y: Expr::Var(Number {
10525                    value: 0.0,
10526                    units: NumericSuffix::Mm,
10527                }),
10528            },
10529            end: Point2d {
10530                x: Expr::Var(Number {
10531                    value: 10.0,
10532                    units: NumericSuffix::Mm,
10533                }),
10534                y: Expr::Var(Number {
10535                    value: 10.0,
10536                    units: NumericSuffix::Mm,
10537                }),
10538            },
10539            center: Point2d {
10540                x: Expr::Var(Number {
10541                    value: 10.0,
10542                    units: NumericSuffix::Mm,
10543                }),
10544                y: Expr::Var(Number {
10545                    value: 0.0,
10546                    units: NumericSuffix::Mm,
10547                }),
10548            },
10549            construction: None,
10550        };
10551        let (_src_delta, scene_delta) = frontend
10552            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10553            .await
10554            .unwrap();
10555        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10556        let arc_id = *scene_delta.new_objects.last().unwrap();
10557
10558        // Add a line segment
10559        let line_ctor = LineCtor {
10560            start: Point2d {
10561                x: Expr::Var(Number {
10562                    value: 20.0,
10563                    units: NumericSuffix::Mm,
10564                }),
10565                y: Expr::Var(Number {
10566                    value: 0.0,
10567                    units: NumericSuffix::Mm,
10568                }),
10569            },
10570            end: Point2d {
10571                x: Expr::Var(Number {
10572                    value: 30.0,
10573                    units: NumericSuffix::Mm,
10574                }),
10575                y: Expr::Var(Number {
10576                    value: 10.0,
10577                    units: NumericSuffix::Mm,
10578                }),
10579            },
10580            construction: None,
10581        };
10582        let (_src_delta, scene_delta) = frontend
10583            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10584            .await
10585            .unwrap();
10586        // The line is the last object in new_objects (after the 2 points: start, end)
10587        let line_id = *scene_delta.new_objects.last().unwrap();
10588
10589        // Attempt to add an invalid coincident constraint between arc and line
10590        // This should fail during execution, but state should remain intact
10591        let constraint = Constraint::Coincident(Coincident {
10592            segments: vec![arc_id.into(), line_id.into()],
10593        });
10594        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10595
10596        // The constraint addition should fail (invalid constraint)
10597        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10598
10599        // Verify state is not corrupted by checking that we can still access the scene graph
10600        // and that the original segments are still present with their source ranges
10601        let sketch_object_after =
10602            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10603        let sketch_after = expect_sketch(sketch_object_after);
10604
10605        // Verify both segments are still in the sketch
10606        assert!(
10607            sketch_after.segments.contains(&arc_id),
10608            "Arc segment should still exist after failed constraint"
10609        );
10610        assert!(
10611            sketch_after.segments.contains(&line_id),
10612            "Line segment should still exist after failed constraint"
10613        );
10614
10615        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10616        let arc_obj = frontend
10617            .scene_graph
10618            .objects
10619            .get(arc_id.0)
10620            .expect("Arc object should still be accessible");
10621        let line_obj = frontend
10622            .scene_graph
10623            .objects
10624            .get(line_id.0)
10625            .expect("Line object should still be accessible");
10626
10627        // Verify source ranges are still valid (not corrupted)
10628        // Just verify that the objects are still accessible and have the expected types
10629        match &arc_obj.kind {
10630            ObjectKind::Segment {
10631                segment: Segment::Arc(_),
10632            } => {}
10633            _ => panic!("Arc object should still be an arc segment"),
10634        }
10635        match &line_obj.kind {
10636            ObjectKind::Segment {
10637                segment: Segment::Line(_),
10638            } => {}
10639            _ => panic!("Line object should still be a line segment"),
10640        }
10641
10642        ctx.close().await;
10643        mock_ctx.close().await;
10644    }
10645
10646    #[tokio::test(flavor = "multi_thread")]
10647    async fn test_distance_two_points() {
10648        let initial_source = "\
10649sketch(on = XY) {
10650  point(at = [var 1, var 2])
10651  point(at = [var 3, var 4])
10652}
10653";
10654
10655        let program = Program::parse(initial_source).unwrap().0.unwrap();
10656
10657        let mut frontend = FrontendState::new();
10658
10659        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10660        let mock_ctx = ExecutorContext::new_mock(None).await;
10661        let version = Version(0);
10662
10663        frontend.hack_set_program(&ctx, program).await.unwrap();
10664        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10665        let sketch_id = sketch_object.id;
10666        let sketch = expect_sketch(sketch_object);
10667        let point0_id = *sketch.segments.first().unwrap();
10668        let point1_id = *sketch.segments.get(1).unwrap();
10669
10670        let constraint = Constraint::Distance(Distance {
10671            points: vec![point0_id.into(), point1_id.into()],
10672            distance: Number {
10673                value: 2.0,
10674                units: NumericSuffix::Mm,
10675            },
10676            label_position: None,
10677            source: Default::default(),
10678        });
10679        let (src_delta, scene_delta) = frontend
10680            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10681            .await
10682            .unwrap();
10683        assert_eq!(
10684            src_delta.text.as_str(),
10685            // The lack indentation is a formatter bug.
10686            "\
10687sketch(on = XY) {
10688  point1 = point(at = [var 1.29, var 2.29])
10689  point2 = point(at = [var 2.71, var 3.71])
10690  distance([point1, point2]) == 2mm
10691}
10692"
10693        );
10694        assert_eq!(
10695            scene_delta.new_graph.objects.len(),
10696            5,
10697            "{:#?}",
10698            scene_delta.new_graph.objects
10699        );
10700
10701        ctx.close().await;
10702        mock_ctx.close().await;
10703    }
10704
10705    #[tokio::test(flavor = "multi_thread")]
10706    async fn test_distance_two_points_with_label() {
10707        let initial_source = "\
10708sketch(on = XY) {
10709  point(at = [var 1, var 2])
10710  point(at = [var 3, var 4])
10711}
10712";
10713
10714        let program = Program::parse(initial_source).unwrap().0.unwrap();
10715
10716        let mut frontend = FrontendState::new();
10717
10718        let mock_ctx = ExecutorContext::new_mock(None).await;
10719        let version = Version(0);
10720
10721        frontend.program = program.clone();
10722        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10723        frontend.update_state_after_exec(outcome, true);
10724        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10725        let sketch_id = sketch_object.id;
10726        let sketch = expect_sketch(sketch_object);
10727        let point0_id = *sketch.segments.first().unwrap();
10728        let point1_id = *sketch.segments.get(1).unwrap();
10729
10730        let label_position = Point2d {
10731            x: Number {
10732                value: 10.0,
10733                units: NumericSuffix::Mm,
10734            },
10735            y: Number {
10736                value: 11.0,
10737                units: NumericSuffix::Mm,
10738            },
10739        };
10740        let constraint = Constraint::Distance(Distance {
10741            points: vec![point0_id.into(), point1_id.into()],
10742            distance: Number {
10743                value: 2.0,
10744                units: NumericSuffix::Mm,
10745            },
10746            label_position: Some(label_position.clone()),
10747            source: Default::default(),
10748        });
10749        let (src_delta, scene_delta) = frontend
10750            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10751            .await
10752            .unwrap();
10753        assert_eq!(
10754            src_delta.text.as_str(),
10755            "\
10756sketch(on = XY) {
10757  point1 = point(at = [var 1.29, var 2.29])
10758  point2 = point(at = [var 2.71, var 3.71])
10759  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10760}
10761"
10762        );
10763
10764        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10765        let sketch = expect_sketch(sketch_object);
10766        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10767        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10768            panic!("Expected constraint object");
10769        };
10770        let Constraint::Distance(distance) = constraint else {
10771            panic!("Expected distance constraint");
10772        };
10773        assert_eq!(distance.label_position, Some(label_position));
10774
10775        mock_ctx.close().await;
10776    }
10777
10778    #[tokio::test(flavor = "multi_thread")]
10779    async fn test_edit_distance_constraint_label_position() {
10780        let initial_source = "\
10781sketch(on = XY) {
10782  point(at = [var 1, var 2])
10783  point(at = [var 3, var 2])
10784}
10785";
10786
10787        let program = Program::parse(initial_source).unwrap().0.unwrap();
10788
10789        let mut frontend = FrontendState::new();
10790
10791        let mock_ctx = ExecutorContext::new_mock(None).await;
10792        let version = Version(0);
10793
10794        frontend.program = program.clone();
10795        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10796        frontend.update_state_after_exec(outcome, true);
10797        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10798        let sketch_id = sketch_object.id;
10799        let sketch = expect_sketch(sketch_object);
10800        let point0_id = *sketch.segments.first().unwrap();
10801        let point1_id = *sketch.segments.get(1).unwrap();
10802
10803        let constraint = Constraint::Distance(Distance {
10804            points: vec![point0_id.into(), point1_id.into()],
10805            distance: Number {
10806                value: 2.0,
10807                units: NumericSuffix::Mm,
10808            },
10809            label_position: None,
10810            source: Default::default(),
10811        });
10812        let (_, scene_delta) = frontend
10813            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10814            .await
10815            .unwrap();
10816        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10817        let sketch = expect_sketch(sketch_object);
10818        let constraint_id = sketch.constraints[0];
10819        let label_position = Point2d {
10820            x: Number {
10821                value: 10.0,
10822                units: NumericSuffix::Mm,
10823            },
10824            y: Number {
10825                value: 11.0,
10826                units: NumericSuffix::Mm,
10827            },
10828        };
10829
10830        let (src_delta, scene_delta) = frontend
10831            .edit_distance_constraint_label_position(
10832                &mock_ctx,
10833                version,
10834                sketch_id,
10835                constraint_id,
10836                label_position.clone(),
10837                vec![],
10838            )
10839            .await
10840            .unwrap();
10841        assert_eq!(
10842            src_delta.text.as_str(),
10843            "\
10844sketch(on = XY) {
10845  point1 = point(at = [var 1, var 2])
10846  point2 = point(at = [var 3, var 2])
10847  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10848}
10849"
10850        );
10851
10852        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10853        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10854            panic!("Expected constraint object");
10855        };
10856        let Constraint::Distance(distance) = constraint else {
10857            panic!("Expected distance constraint");
10858        };
10859        assert_eq!(distance.label_position, Some(label_position));
10860
10861        mock_ctx.close().await;
10862    }
10863
10864    #[tokio::test(flavor = "multi_thread")]
10865    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10866        let initial_source = "\
10867sketch(on = XY) {
10868  point1 = point(at = [var 0mm, var 0mm])
10869  point2 = point(at = [var 10mm, var 0mm])
10870  distance([point1, point2]) == 5mm
10871}
10872";
10873
10874        let program = Program::parse(initial_source).unwrap().0.unwrap();
10875        let mut frontend = FrontendState::new();
10876        let mock_ctx = ExecutorContext::new_mock(None).await;
10877        let version = Version(0);
10878
10879        frontend.program = program.clone();
10880        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10881        frontend.update_state_after_exec(outcome, true);
10882        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10883        let sketch_id = sketch_object.id;
10884        let sketch = expect_sketch(sketch_object);
10885        let point0_id = sketch.segments[0];
10886        let point1_id = sketch.segments[1];
10887        let constraint_id = sketch.constraints[0];
10888
10889        let edited_segments = vec![ExistingSegmentCtor {
10890            id: point0_id,
10891            ctor: SegmentCtor::Point(PointCtor {
10892                position: Point2d {
10893                    x: Expr::Var(Number {
10894                        value: 2.0,
10895                        units: NumericSuffix::Mm,
10896                    }),
10897                    y: Expr::Var(Number {
10898                        value: 1.0,
10899                        units: NumericSuffix::Mm,
10900                    }),
10901                },
10902            }),
10903        }];
10904        let (_, scene_delta) = frontend
10905            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10906            .await
10907            .unwrap();
10908        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10909        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10910
10911        let label_position = Point2d {
10912            x: Number {
10913                value: 3.0,
10914                units: NumericSuffix::Mm,
10915            },
10916            y: Number {
10917                value: 4.0,
10918                units: NumericSuffix::Mm,
10919            },
10920        };
10921        let (_, scene_delta) = frontend
10922            .edit_distance_constraint_label_position(
10923                &mock_ctx,
10924                version,
10925                sketch_id,
10926                constraint_id,
10927                label_position,
10928                vec![point0_id],
10929            )
10930            .await
10931            .unwrap();
10932
10933        assert_point_position_close(
10934            point_position(&scene_delta.new_graph, point0_id),
10935            point0_after_segment_edit,
10936        );
10937        assert_point_position_close(
10938            point_position(&scene_delta.new_graph, point1_id),
10939            point1_after_segment_edit,
10940        );
10941
10942        mock_ctx.close().await;
10943    }
10944
10945    #[tokio::test(flavor = "multi_thread")]
10946    async fn test_distance_point_line() {
10947        let initial_source = "\
10948sketch(on = XY) {
10949  point(at = [var 0, var 5])
10950  line(start = [var 0, var 0], end = [var 10, var 0])
10951}
10952";
10953
10954        let program = Program::parse(initial_source).unwrap().0.unwrap();
10955
10956        let mut frontend = FrontendState::new();
10957
10958        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10959        let mock_ctx = ExecutorContext::new_mock(None).await;
10960        let version = Version(0);
10961
10962        frontend.hack_set_program(&ctx, program).await.unwrap();
10963        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10964        let sketch_id = sketch_object.id;
10965        let sketch = expect_sketch(sketch_object);
10966        let point_id = *sketch.segments.first().unwrap();
10967        let line_id = *sketch
10968            .segments
10969            .iter()
10970            .find(|segment_id| {
10971                matches!(
10972                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10973                    Some(ObjectKind::Segment {
10974                        segment: Segment::Line(_)
10975                    })
10976                )
10977            })
10978            .unwrap();
10979
10980        let label_position = Point2d {
10981            x: Number {
10982                value: 10.0,
10983                units: NumericSuffix::Mm,
10984            },
10985            y: Number {
10986                value: 11.0,
10987                units: NumericSuffix::Mm,
10988            },
10989        };
10990        let constraint = Constraint::Distance(Distance {
10991            points: vec![point_id.into(), line_id.into()],
10992            distance: Number {
10993                value: 5.0,
10994                units: NumericSuffix::Mm,
10995            },
10996            label_position: Some(label_position.clone()),
10997            source: Default::default(),
10998        });
10999        let (src_delta, scene_delta) = frontend
11000            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11001            .await
11002            .unwrap();
11003        assert_eq!(
11004            src_delta.text.as_str(),
11005            "\
11006sketch(on = XY) {
11007  point1 = point(at = [var 0, var 5])
11008  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11009  distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11010}
11011"
11012        );
11013        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11014        let sketch = expect_sketch(sketch_object);
11015        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11016        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11017            panic!("Expected constraint object");
11018        };
11019        let Constraint::Distance(distance) = constraint else {
11020            panic!("Expected distance constraint");
11021        };
11022        assert_eq!(distance.label_position, Some(label_position));
11023
11024        ctx.close().await;
11025        mock_ctx.close().await;
11026    }
11027
11028    #[tokio::test(flavor = "multi_thread")]
11029    async fn test_distance_point_arc() {
11030        let initial_source = "\
11031sketch(on = XY) {
11032  point(at = [var 0, var 8])
11033  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11034}
11035";
11036
11037        let program = Program::parse(initial_source).unwrap().0.unwrap();
11038
11039        let mut frontend = FrontendState::new();
11040
11041        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11042        let mock_ctx = ExecutorContext::new_mock(None).await;
11043        let version = Version(0);
11044
11045        frontend.hack_set_program(&ctx, program).await.unwrap();
11046        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11047        let sketch_id = sketch_object.id;
11048        let sketch = expect_sketch(sketch_object);
11049        let point_id = *sketch.segments.first().unwrap();
11050        let arc_id = *sketch
11051            .segments
11052            .iter()
11053            .find(|segment_id| {
11054                matches!(
11055                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11056                    Some(ObjectKind::Segment {
11057                        segment: Segment::Arc(_)
11058                    })
11059                )
11060            })
11061            .unwrap();
11062
11063        let constraint = Constraint::Distance(Distance {
11064            points: vec![point_id.into(), arc_id.into()],
11065            distance: Number {
11066                value: 3.0,
11067                units: NumericSuffix::Mm,
11068            },
11069            label_position: None,
11070            source: Default::default(),
11071        });
11072        let (src_delta, _scene_delta) = frontend
11073            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11074            .await
11075            .unwrap();
11076        assert_eq!(
11077            src_delta.text.as_str(),
11078            "\
11079sketch(on = XY) {
11080  point1 = point(at = [var 0, var 8])
11081  arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11082  distance([point1, arc1]) == 3mm
11083}
11084"
11085        );
11086
11087        ctx.close().await;
11088        mock_ctx.close().await;
11089    }
11090
11091    #[tokio::test(flavor = "multi_thread")]
11092    async fn test_distance_arc_origin() {
11093        let initial_source = "\
11094sketch001 = sketch(on = XY) {
11095  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11096}
11097";
11098
11099        let program = Program::parse(initial_source).unwrap().0.unwrap();
11100
11101        let mut frontend = FrontendState::new();
11102
11103        let mock_ctx = ExecutorContext::new_mock(None).await;
11104        let version = Version(0);
11105
11106        frontend.program = program.clone();
11107        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11108        frontend.update_state_after_exec(outcome, true);
11109        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11110        let sketch_id = sketch_object.id;
11111        let sketch = expect_sketch(sketch_object);
11112        let arc_id = *sketch
11113            .segments
11114            .iter()
11115            .find(|segment_id| {
11116                matches!(
11117                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11118                    Some(ObjectKind::Segment {
11119                        segment: Segment::Arc(_)
11120                    })
11121                )
11122            })
11123            .unwrap();
11124
11125        let constraint = Constraint::Distance(Distance {
11126            points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11127            distance: Number {
11128                value: 3.0,
11129                units: NumericSuffix::Mm,
11130            },
11131            label_position: None,
11132            source: Default::default(),
11133        });
11134        let (src_delta, _scene_delta) = frontend
11135            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11136            .await
11137            .unwrap();
11138        assert_eq!(
11139            src_delta.text.as_str(),
11140            "\
11141sketch001 = sketch(on = XY) {
11142  arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11143  distance([arc1, ORIGIN]) == 3mm
11144}
11145"
11146        );
11147
11148        mock_ctx.close().await;
11149    }
11150
11151    #[tokio::test(flavor = "multi_thread")]
11152    async fn test_distance_line_origin() {
11153        let initial_source = "\
11154sketch(on = XY) {
11155  line(start = [var 5, var 0], end = [var 5, var 10])
11156}
11157";
11158
11159        let program = Program::parse(initial_source).unwrap().0.unwrap();
11160
11161        let mut frontend = FrontendState::new();
11162
11163        let mock_ctx = ExecutorContext::new_mock(None).await;
11164        let version = Version(0);
11165
11166        frontend.program = program.clone();
11167        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11168        frontend.update_state_after_exec(outcome, true);
11169        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11170        let sketch_id = sketch_object.id;
11171        let sketch = expect_sketch(sketch_object);
11172        let line_id = *sketch
11173            .segments
11174            .iter()
11175            .find(|segment_id| {
11176                matches!(
11177                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11178                    Some(ObjectKind::Segment {
11179                        segment: Segment::Line(_)
11180                    })
11181                )
11182            })
11183            .unwrap();
11184
11185        let constraint = Constraint::Distance(Distance {
11186            points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11187            distance: Number {
11188                value: 5.0,
11189                units: NumericSuffix::Mm,
11190            },
11191            label_position: None,
11192            source: Default::default(),
11193        });
11194        let (src_delta, _scene_delta) = frontend
11195            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11196            .await
11197            .unwrap();
11198        assert_eq!(
11199            src_delta.text.as_str(),
11200            "\
11201sketch(on = XY) {
11202  line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11203  distance([ORIGIN, line1]) == 5mm
11204}
11205"
11206        );
11207
11208        mock_ctx.close().await;
11209    }
11210
11211    #[tokio::test(flavor = "multi_thread")]
11212    async fn test_distance_line_circle() {
11213        let initial_source = "\
11214sketch(on = XY) {
11215  line(start = [var -10, var 8], end = [var 10, var 8])
11216  circle(start = [var 5, var 0], center = [var 0, var 0])
11217}
11218";
11219
11220        let program = Program::parse(initial_source).unwrap().0.unwrap();
11221
11222        let mut frontend = FrontendState::new();
11223
11224        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11225        let mock_ctx = ExecutorContext::new_mock(None).await;
11226        let version = Version(0);
11227
11228        frontend.hack_set_program(&ctx, program).await.unwrap();
11229        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11230        let sketch_id = sketch_object.id;
11231        let sketch = expect_sketch(sketch_object);
11232        let line_id = *sketch
11233            .segments
11234            .iter()
11235            .find(|segment_id| {
11236                matches!(
11237                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11238                    Some(ObjectKind::Segment {
11239                        segment: Segment::Line(_)
11240                    })
11241                )
11242            })
11243            .unwrap();
11244        let circle_id = *sketch
11245            .segments
11246            .iter()
11247            .find(|segment_id| {
11248                matches!(
11249                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11250                    Some(ObjectKind::Segment {
11251                        segment: Segment::Circle(_)
11252                    })
11253                )
11254            })
11255            .unwrap();
11256
11257        let constraint = Constraint::Distance(Distance {
11258            points: vec![line_id.into(), circle_id.into()],
11259            distance: Number {
11260                value: 3.0,
11261                units: NumericSuffix::Mm,
11262            },
11263            label_position: None,
11264            source: Default::default(),
11265        });
11266        let (src_delta, _scene_delta) = frontend
11267            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11268            .await
11269            .unwrap();
11270        assert_eq!(
11271            src_delta.text.as_str(),
11272            "\
11273sketch(on = XY) {
11274  line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11275  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11276  distance([line1, circle1]) == 3mm
11277}
11278"
11279        );
11280
11281        ctx.close().await;
11282        mock_ctx.close().await;
11283    }
11284
11285    #[tokio::test(flavor = "multi_thread")]
11286    async fn test_distance_circle_arc() {
11287        let initial_source = "\
11288sketch(on = XY) {
11289  circle(start = [var 5, var 0], center = [var 0, var 0])
11290  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11291}
11292";
11293
11294        let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296        let mut frontend = FrontendState::new();
11297
11298        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11299        let mock_ctx = ExecutorContext::new_mock(None).await;
11300        let version = Version(0);
11301
11302        frontend.hack_set_program(&ctx, program).await.unwrap();
11303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11304        let sketch_id = sketch_object.id;
11305        let sketch = expect_sketch(sketch_object);
11306        let circle_id = *sketch
11307            .segments
11308            .iter()
11309            .find(|segment_id| {
11310                matches!(
11311                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11312                    Some(ObjectKind::Segment {
11313                        segment: Segment::Circle(_)
11314                    })
11315                )
11316            })
11317            .unwrap();
11318        let arc_id = *sketch
11319            .segments
11320            .iter()
11321            .find(|segment_id| {
11322                matches!(
11323                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11324                    Some(ObjectKind::Segment {
11325                        segment: Segment::Arc(_)
11326                    })
11327                )
11328            })
11329            .unwrap();
11330
11331        let constraint = Constraint::Distance(Distance {
11332            points: vec![circle_id.into(), arc_id.into()],
11333            distance: Number {
11334                value: 3.0,
11335                units: NumericSuffix::Mm,
11336            },
11337            label_position: None,
11338            source: Default::default(),
11339        });
11340        let (src_delta, _scene_delta) = frontend
11341            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11342            .await
11343            .unwrap();
11344        assert_eq!(
11345            src_delta.text.as_str(),
11346            "\
11347sketch(on = XY) {
11348  circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11349  arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11350  distance([circle1, arc1]) == 3mm
11351}
11352"
11353        );
11354
11355        ctx.close().await;
11356        mock_ctx.close().await;
11357    }
11358
11359    #[tokio::test(flavor = "multi_thread")]
11360    async fn test_distance_parallel_lines() {
11361        let initial_source = "\
11362sketch(on = XY) {
11363  line(start = [var 0, var 0], end = [var 10, var 0])
11364  line(start = [var 0, var 5], end = [var 10, var 5])
11365}
11366";
11367
11368        let program = Program::parse(initial_source).unwrap().0.unwrap();
11369
11370        let mut frontend = FrontendState::new();
11371
11372        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11373        let mock_ctx = ExecutorContext::new_mock(None).await;
11374        let version = Version(0);
11375
11376        frontend.hack_set_program(&ctx, program).await.unwrap();
11377        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11378        let sketch_id = sketch_object.id;
11379        let sketch = expect_sketch(sketch_object);
11380        let line_ids = sketch
11381            .segments
11382            .iter()
11383            .copied()
11384            .filter(|segment_id| {
11385                matches!(
11386                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11387                    Some(ObjectKind::Segment {
11388                        segment: Segment::Line(_)
11389                    })
11390                )
11391            })
11392            .collect::<Vec<_>>();
11393
11394        let constraint = Constraint::Distance(Distance {
11395            points: vec![line_ids[0].into(), line_ids[1].into()],
11396            distance: Number {
11397                value: 5.0,
11398                units: NumericSuffix::Mm,
11399            },
11400            label_position: None,
11401            source: Default::default(),
11402        });
11403        let (src_delta, _scene_delta) = frontend
11404            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11405            .await
11406            .unwrap();
11407        assert_eq!(
11408            src_delta.text.as_str(),
11409            "\
11410sketch(on = XY) {
11411  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11412  line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11413  distance([line1, line2]) == 5mm
11414}
11415"
11416        );
11417
11418        ctx.close().await;
11419        mock_ctx.close().await;
11420    }
11421
11422    #[tokio::test(flavor = "multi_thread")]
11423    async fn test_distance_non_parallel_lines_lowers_to_distance() {
11424        let initial_source = "\
11425sketch(on = XY) {
11426  line(start = [var 0, var 0], end = [var 10, var 0])
11427  line(start = [var 0, var 0], end = [var 0, var 10])
11428}
11429";
11430
11431        let program = Program::parse(initial_source).unwrap().0.unwrap();
11432
11433        let mut frontend = FrontendState::new();
11434
11435        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11436        let mock_ctx = ExecutorContext::new_mock(None).await;
11437        let version = Version(0);
11438
11439        frontend.hack_set_program(&ctx, program).await.unwrap();
11440        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11441        let sketch_id = sketch_object.id;
11442        let sketch = expect_sketch(sketch_object);
11443        let line_ids = sketch
11444            .segments
11445            .iter()
11446            .copied()
11447            .filter(|segment_id| {
11448                matches!(
11449                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11450                    Some(ObjectKind::Segment {
11451                        segment: Segment::Line(_)
11452                    })
11453                )
11454            })
11455            .collect::<Vec<_>>();
11456
11457        let constraint = Constraint::Distance(Distance {
11458            points: vec![line_ids[0].into(), line_ids[1].into()],
11459            distance: Number {
11460                value: 5.0,
11461                units: NumericSuffix::Mm,
11462            },
11463            label_position: None,
11464            source: Default::default(),
11465        });
11466        let (src_delta, _scene_delta) = frontend
11467            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11468            .await
11469            .unwrap();
11470        assert_eq!(
11471            src_delta.text.as_str(),
11472            "\
11473sketch(on = XY) {
11474  line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11475  line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11476  distance([line1, line2]) == 5mm
11477}
11478"
11479        );
11480
11481        ctx.close().await;
11482        mock_ctx.close().await;
11483    }
11484
11485    #[tokio::test(flavor = "multi_thread")]
11486    async fn test_horizontal_distance_two_points() {
11487        let initial_source = "\
11488sketch(on = XY) {
11489  point(at = [var 1, var 2])
11490  point(at = [var 3, var 4])
11491}
11492";
11493
11494        let program = Program::parse(initial_source).unwrap().0.unwrap();
11495
11496        let mut frontend = FrontendState::new();
11497
11498        let mock_ctx = ExecutorContext::new_mock(None).await;
11499        let version = Version(0);
11500
11501        frontend.program = program.clone();
11502        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11503        frontend.update_state_after_exec(outcome, true);
11504        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11505        let sketch_id = sketch_object.id;
11506        let sketch = expect_sketch(sketch_object);
11507        let point0_id = *sketch.segments.first().unwrap();
11508        let point1_id = *sketch.segments.get(1).unwrap();
11509        let label_position = Point2d {
11510            x: Number {
11511                value: 10.0,
11512                units: NumericSuffix::Mm,
11513            },
11514            y: Number {
11515                value: 11.0,
11516                units: NumericSuffix::Mm,
11517            },
11518        };
11519
11520        let constraint = Constraint::HorizontalDistance(Distance {
11521            points: vec![point0_id.into(), point1_id.into()],
11522            distance: Number {
11523                value: 2.0,
11524                units: NumericSuffix::Mm,
11525            },
11526            label_position: Some(label_position.clone()),
11527            source: Default::default(),
11528        });
11529        let (src_delta, scene_delta) = frontend
11530            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11531            .await
11532            .unwrap();
11533        assert_eq!(
11534            src_delta.text.as_str(),
11535            // The lack indentation is a formatter bug.
11536            "\
11537sketch(on = XY) {
11538  point1 = point(at = [var 1, var 2])
11539  point2 = point(at = [var 3, var 4])
11540  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11541}
11542"
11543        );
11544        assert_eq!(
11545            scene_delta.new_graph.objects.len(),
11546            5,
11547            "{:#?}",
11548            scene_delta.new_graph.objects
11549        );
11550        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11551        let sketch = expect_sketch(sketch_object);
11552        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11553        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11554            panic!("Expected constraint object");
11555        };
11556        let Constraint::HorizontalDistance(distance) = constraint else {
11557            panic!("Expected horizontal distance constraint");
11558        };
11559        assert_eq!(distance.label_position, Some(label_position));
11560
11561        mock_ctx.close().await;
11562    }
11563
11564    #[tokio::test(flavor = "multi_thread")]
11565    async fn test_radius_single_arc_segment() {
11566        let initial_source = "\
11567sketch(on = XY) {
11568  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11569}
11570";
11571
11572        let program = Program::parse(initial_source).unwrap().0.unwrap();
11573
11574        let mut frontend = FrontendState::new();
11575
11576        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11577        let mock_ctx = ExecutorContext::new_mock(None).await;
11578        let version = Version(0);
11579
11580        frontend.hack_set_program(&ctx, program).await.unwrap();
11581        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11582        let sketch_id = sketch_object.id;
11583        let sketch = expect_sketch(sketch_object);
11584        // Find the arc segment (not the points)
11585        let arc_id = sketch
11586            .segments
11587            .iter()
11588            .find(|&seg_id| {
11589                let obj = frontend.scene_graph.objects.get(seg_id.0);
11590                matches!(
11591                    obj.map(|o| &o.kind),
11592                    Some(ObjectKind::Segment {
11593                        segment: Segment::Arc(_)
11594                    })
11595                )
11596            })
11597            .unwrap();
11598
11599        let constraint = Constraint::Radius(Radius {
11600            arc: *arc_id,
11601            radius: Number {
11602                value: 5.0,
11603                units: NumericSuffix::Mm,
11604            },
11605            label_position: None,
11606            source: Default::default(),
11607        });
11608        let (src_delta, scene_delta) = frontend
11609            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11610            .await
11611            .unwrap();
11612        assert_eq!(
11613            src_delta.text.as_str(),
11614            // The lack indentation is a formatter bug.
11615            "\
11616sketch(on = XY) {
11617  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11618  radius(arc1) == 5mm
11619}
11620"
11621        );
11622        assert_eq!(
11623            scene_delta.new_graph.objects.len(),
11624            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
11625            "{:#?}",
11626            scene_delta.new_graph.objects
11627        );
11628
11629        ctx.close().await;
11630        mock_ctx.close().await;
11631    }
11632
11633    #[tokio::test(flavor = "multi_thread")]
11634    async fn test_radius_single_arc_segment_with_label_position() {
11635        let initial_source = "\
11636sketch(on = XY) {
11637  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11638}
11639";
11640
11641        let program = Program::parse(initial_source).unwrap().0.unwrap();
11642        let mut frontend = FrontendState::new();
11643        let mock_ctx = ExecutorContext::new_mock(None).await;
11644        let version = Version(0);
11645
11646        frontend.program = program.clone();
11647        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11648        frontend.update_state_after_exec(outcome, true);
11649        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11650        let sketch_id = sketch_object.id;
11651        let sketch = expect_sketch(sketch_object);
11652        let arc_id = sketch
11653            .segments
11654            .iter()
11655            .find(|&seg_id| {
11656                let obj = frontend.scene_graph.objects.get(seg_id.0);
11657                matches!(
11658                    obj.map(|o| &o.kind),
11659                    Some(ObjectKind::Segment {
11660                        segment: Segment::Arc(_)
11661                    })
11662                )
11663            })
11664            .unwrap();
11665
11666        let label_position = Point2d {
11667            x: Number {
11668                value: 10.0,
11669                units: NumericSuffix::Mm,
11670            },
11671            y: Number {
11672                value: 11.0,
11673                units: NumericSuffix::Mm,
11674            },
11675        };
11676        let constraint = Constraint::Radius(Radius {
11677            arc: *arc_id,
11678            radius: Number {
11679                value: 5.0,
11680                units: NumericSuffix::Mm,
11681            },
11682            label_position: Some(label_position.clone()),
11683            source: Default::default(),
11684        });
11685        let (src_delta, scene_delta) = frontend
11686            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11687            .await
11688            .unwrap();
11689        assert_eq!(
11690            src_delta.text.as_str(),
11691            "\
11692sketch(on = XY) {
11693  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11694  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11695}
11696"
11697        );
11698
11699        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11700        let sketch = expect_sketch(sketch_object);
11701        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11702        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11703            panic!("Expected constraint object");
11704        };
11705        let Constraint::Radius(radius) = constraint else {
11706            panic!("Expected radius constraint");
11707        };
11708        assert_eq!(radius.label_position, Some(label_position));
11709
11710        mock_ctx.close().await;
11711    }
11712
11713    #[tokio::test(flavor = "multi_thread")]
11714    async fn test_edit_radius_constraint_label_position() {
11715        let initial_source = "\
11716sketch(on = XY) {
11717  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11718  radius(arc1) == 5mm
11719}
11720";
11721
11722        let program = Program::parse(initial_source).unwrap().0.unwrap();
11723        let mut frontend = FrontendState::new();
11724        let mock_ctx = ExecutorContext::new_mock(None).await;
11725        let version = Version(0);
11726
11727        frontend.program = program.clone();
11728        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11729        frontend.update_state_after_exec(outcome, true);
11730        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11731        let sketch_id = sketch_object.id;
11732        let sketch = expect_sketch(sketch_object);
11733        let constraint_id = sketch.constraints[0];
11734        let label_position = Point2d {
11735            x: Number {
11736                value: 10.0,
11737                units: NumericSuffix::Mm,
11738            },
11739            y: Number {
11740                value: 11.0,
11741                units: NumericSuffix::Mm,
11742            },
11743        };
11744
11745        let (src_delta, scene_delta) = frontend
11746            .edit_distance_constraint_label_position(
11747                &mock_ctx,
11748                version,
11749                sketch_id,
11750                constraint_id,
11751                label_position.clone(),
11752                vec![],
11753            )
11754            .await
11755            .unwrap();
11756        assert_eq!(
11757            src_delta.text.as_str(),
11758            "\
11759sketch(on = XY) {
11760  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11761  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11762}
11763"
11764        );
11765
11766        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11767        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11768            panic!("Expected constraint object");
11769        };
11770        let Constraint::Radius(radius) = constraint else {
11771            panic!("Expected radius constraint");
11772        };
11773        assert_eq!(radius.label_position, Some(label_position));
11774
11775        mock_ctx.close().await;
11776    }
11777
11778    #[tokio::test(flavor = "multi_thread")]
11779    async fn test_vertical_distance_two_points() {
11780        let initial_source = "\
11781sketch(on = XY) {
11782  point(at = [var 1, var 2])
11783  point(at = [var 3, var 4])
11784}
11785";
11786
11787        let program = Program::parse(initial_source).unwrap().0.unwrap();
11788
11789        let mut frontend = FrontendState::new();
11790
11791        let mock_ctx = ExecutorContext::new_mock(None).await;
11792        let version = Version(0);
11793
11794        frontend.program = program.clone();
11795        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11796        frontend.update_state_after_exec(outcome, true);
11797        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11798        let sketch_id = sketch_object.id;
11799        let sketch = expect_sketch(sketch_object);
11800        let point0_id = *sketch.segments.first().unwrap();
11801        let point1_id = *sketch.segments.get(1).unwrap();
11802        let label_position = Point2d {
11803            x: Number {
11804                value: 10.0,
11805                units: NumericSuffix::Mm,
11806            },
11807            y: Number {
11808                value: 11.0,
11809                units: NumericSuffix::Mm,
11810            },
11811        };
11812
11813        let constraint = Constraint::VerticalDistance(Distance {
11814            points: vec![point0_id.into(), point1_id.into()],
11815            distance: Number {
11816                value: 2.0,
11817                units: NumericSuffix::Mm,
11818            },
11819            label_position: Some(label_position.clone()),
11820            source: Default::default(),
11821        });
11822        let (src_delta, scene_delta) = frontend
11823            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11824            .await
11825            .unwrap();
11826        assert_eq!(
11827            src_delta.text.as_str(),
11828            // The lack indentation is a formatter bug.
11829            "\
11830sketch(on = XY) {
11831  point1 = point(at = [var 1, var 2])
11832  point2 = point(at = [var 3, var 4])
11833  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11834}
11835"
11836        );
11837        assert_eq!(
11838            scene_delta.new_graph.objects.len(),
11839            5,
11840            "{:#?}",
11841            scene_delta.new_graph.objects
11842        );
11843        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11844        let sketch = expect_sketch(sketch_object);
11845        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11846        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11847            panic!("Expected constraint object");
11848        };
11849        let Constraint::VerticalDistance(distance) = constraint else {
11850            panic!("Expected vertical distance constraint");
11851        };
11852        assert_eq!(distance.label_position, Some(label_position));
11853
11854        mock_ctx.close().await;
11855    }
11856
11857    #[tokio::test(flavor = "multi_thread")]
11858    async fn test_add_fixed_standalone_point() {
11859        let initial_source = "\
11860sketch(on = XY) {
11861  point(at = [var 1, var 2])
11862}
11863";
11864
11865        let program = Program::parse(initial_source).unwrap().0.unwrap();
11866
11867        let mut frontend = FrontendState::new();
11868
11869        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11870        let mock_ctx = ExecutorContext::new_mock(None).await;
11871        let version = Version(0);
11872
11873        frontend.hack_set_program(&ctx, program).await.unwrap();
11874        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11875        let sketch_id = sketch_object.id;
11876        let sketch = expect_sketch(sketch_object);
11877        let point_id = *sketch.segments.first().unwrap();
11878
11879        let (src_delta, scene_delta) = frontend
11880            .add_constraint(
11881                &mock_ctx,
11882                version,
11883                sketch_id,
11884                Constraint::Fixed(Fixed {
11885                    points: vec![FixedPoint {
11886                        point: point_id,
11887                        position: Point2d {
11888                            x: Number {
11889                                value: 2.0,
11890                                units: NumericSuffix::Mm,
11891                            },
11892                            y: Number {
11893                                value: 3.0,
11894                                units: NumericSuffix::Mm,
11895                            },
11896                        },
11897                    }],
11898                }),
11899            )
11900            .await
11901            .unwrap();
11902        assert_eq!(
11903            src_delta.text.as_str(),
11904            "\
11905sketch(on = XY) {
11906  point1 = point(at = [var 2, var 3])
11907  fixed([point1, [2mm, 3mm]])
11908}
11909"
11910        );
11911        assert_eq!(
11912            scene_delta.new_graph.objects.len(),
11913            4,
11914            "{:#?}",
11915            scene_delta.new_graph.objects
11916        );
11917
11918        ctx.close().await;
11919        mock_ctx.close().await;
11920    }
11921
11922    #[tokio::test(flavor = "multi_thread")]
11923    async fn test_add_fixed_multiple_points() {
11924        let initial_source = "\
11925sketch(on = XY) {
11926  point(at = [var 1, var 2])
11927  point(at = [var 3, var 4])
11928}
11929";
11930
11931        let program = Program::parse(initial_source).unwrap().0.unwrap();
11932
11933        let mut frontend = FrontendState::new();
11934
11935        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11936        let mock_ctx = ExecutorContext::new_mock(None).await;
11937        let version = Version(0);
11938
11939        frontend.hack_set_program(&ctx, program).await.unwrap();
11940        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11941        let sketch_id = sketch_object.id;
11942        let sketch = expect_sketch(sketch_object);
11943        let point0_id = *sketch.segments.first().unwrap();
11944        let point1_id = *sketch.segments.get(1).unwrap();
11945
11946        let (src_delta, scene_delta) = frontend
11947            .add_constraint(
11948                &mock_ctx,
11949                version,
11950                sketch_id,
11951                Constraint::Fixed(Fixed {
11952                    points: vec![
11953                        FixedPoint {
11954                            point: point0_id,
11955                            position: Point2d {
11956                                x: Number {
11957                                    value: 2.0,
11958                                    units: NumericSuffix::Mm,
11959                                },
11960                                y: Number {
11961                                    value: 3.0,
11962                                    units: NumericSuffix::Mm,
11963                                },
11964                            },
11965                        },
11966                        FixedPoint {
11967                            point: point1_id,
11968                            position: Point2d {
11969                                x: Number {
11970                                    value: 4.0,
11971                                    units: NumericSuffix::Mm,
11972                                },
11973                                y: Number {
11974                                    value: 5.0,
11975                                    units: NumericSuffix::Mm,
11976                                },
11977                            },
11978                        },
11979                    ],
11980                }),
11981            )
11982            .await
11983            .unwrap();
11984        assert_eq!(
11985            src_delta.text.as_str(),
11986            "\
11987sketch(on = XY) {
11988  point1 = point(at = [var 2, var 3])
11989  point2 = point(at = [var 4, var 5])
11990  fixed([point1, [2mm, 3mm]])
11991  fixed([point2, [4mm, 5mm]])
11992}
11993"
11994        );
11995        assert_eq!(
11996            scene_delta.new_graph.objects.len(),
11997            6,
11998            "{:#?}",
11999            scene_delta.new_graph.objects
12000        );
12001
12002        ctx.close().await;
12003        mock_ctx.close().await;
12004    }
12005
12006    #[tokio::test(flavor = "multi_thread")]
12007    async fn test_add_fixed_owned_point() {
12008        let initial_source = "\
12009sketch(on = XY) {
12010  line(start = [var 1, var 2], end = [var 3, var 4])
12011}
12012";
12013
12014        let program = Program::parse(initial_source).unwrap().0.unwrap();
12015
12016        let mut frontend = FrontendState::new();
12017
12018        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12019        let mock_ctx = ExecutorContext::new_mock(None).await;
12020        let version = Version(0);
12021
12022        frontend.hack_set_program(&ctx, program).await.unwrap();
12023        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12024        let sketch_id = sketch_object.id;
12025        let sketch = expect_sketch(sketch_object);
12026        let line_start_id = *sketch.segments.first().unwrap();
12027
12028        let (src_delta, scene_delta) = frontend
12029            .add_constraint(
12030                &mock_ctx,
12031                version,
12032                sketch_id,
12033                Constraint::Fixed(Fixed {
12034                    points: vec![FixedPoint {
12035                        point: line_start_id,
12036                        position: Point2d {
12037                            x: Number {
12038                                value: 2.0,
12039                                units: NumericSuffix::Mm,
12040                            },
12041                            y: Number {
12042                                value: 3.0,
12043                                units: NumericSuffix::Mm,
12044                            },
12045                        },
12046                    }],
12047                }),
12048            )
12049            .await
12050            .unwrap();
12051        assert_eq!(
12052            src_delta.text.as_str(),
12053            "\
12054sketch(on = XY) {
12055  line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12056  fixed([line1.start, [2mm, 3mm]])
12057}
12058"
12059        );
12060        assert_eq!(
12061            scene_delta.new_graph.objects.len(),
12062            6,
12063            "{:#?}",
12064            scene_delta.new_graph.objects
12065        );
12066
12067        ctx.close().await;
12068        mock_ctx.close().await;
12069    }
12070
12071    #[tokio::test(flavor = "multi_thread")]
12072    async fn test_radius_error_cases() {
12073        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12074        let mock_ctx = ExecutorContext::new_mock(None).await;
12075        let version = Version(0);
12076
12077        // Test: Single point should error
12078        let initial_source_point = "\
12079sketch(on = XY) {
12080  point(at = [var 1, var 2])
12081}
12082";
12083        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12084        let mut frontend_point = FrontendState::new();
12085        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12086        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12087        let sketch_id_point = sketch_object_point.id;
12088        let sketch_point = expect_sketch(sketch_object_point);
12089        let point_id = *sketch_point.segments.first().unwrap();
12090
12091        let constraint_point = Constraint::Radius(Radius {
12092            arc: point_id,
12093            radius: Number {
12094                value: 5.0,
12095                units: NumericSuffix::Mm,
12096            },
12097            label_position: None,
12098            source: Default::default(),
12099        });
12100        let result_point = frontend_point
12101            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12102            .await;
12103        assert!(result_point.is_err(), "Single point should error for radius");
12104
12105        // Test: Single line segment should error (only arc segments supported)
12106        let initial_source_line = "\
12107sketch(on = XY) {
12108  line(start = [var 1, var 2], end = [var 3, var 4])
12109}
12110";
12111        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12112        let mut frontend_line = FrontendState::new();
12113        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12114        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12115        let sketch_id_line = sketch_object_line.id;
12116        let sketch_line = expect_sketch(sketch_object_line);
12117        let line_id = *sketch_line.segments.first().unwrap();
12118
12119        let constraint_line = Constraint::Radius(Radius {
12120            arc: line_id,
12121            radius: Number {
12122                value: 5.0,
12123                units: NumericSuffix::Mm,
12124            },
12125            label_position: None,
12126            source: Default::default(),
12127        });
12128        let result_line = frontend_line
12129            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12130            .await;
12131        assert!(result_line.is_err(), "Single line segment should error for radius");
12132
12133        ctx.close().await;
12134        mock_ctx.close().await;
12135    }
12136
12137    #[tokio::test(flavor = "multi_thread")]
12138    async fn test_diameter_single_arc_segment() {
12139        let initial_source = "\
12140sketch(on = XY) {
12141  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12142}
12143";
12144
12145        let program = Program::parse(initial_source).unwrap().0.unwrap();
12146
12147        let mut frontend = FrontendState::new();
12148
12149        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12150        let mock_ctx = ExecutorContext::new_mock(None).await;
12151        let version = Version(0);
12152
12153        frontend.hack_set_program(&ctx, program).await.unwrap();
12154        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12155        let sketch_id = sketch_object.id;
12156        let sketch = expect_sketch(sketch_object);
12157        // Find the arc segment (not the points)
12158        let arc_id = sketch
12159            .segments
12160            .iter()
12161            .find(|&seg_id| {
12162                let obj = frontend.scene_graph.objects.get(seg_id.0);
12163                matches!(
12164                    obj.map(|o| &o.kind),
12165                    Some(ObjectKind::Segment {
12166                        segment: Segment::Arc(_)
12167                    })
12168                )
12169            })
12170            .unwrap();
12171
12172        let constraint = Constraint::Diameter(Diameter {
12173            arc: *arc_id,
12174            diameter: Number {
12175                value: 10.0,
12176                units: NumericSuffix::Mm,
12177            },
12178            label_position: None,
12179            source: Default::default(),
12180        });
12181        let (src_delta, scene_delta) = frontend
12182            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12183            .await
12184            .unwrap();
12185        assert_eq!(
12186            src_delta.text.as_str(),
12187            // The lack indentation is a formatter bug.
12188            "\
12189sketch(on = XY) {
12190  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12191  diameter(arc1) == 10mm
12192}
12193"
12194        );
12195        assert_eq!(
12196            scene_delta.new_graph.objects.len(),
12197            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12198            "{:#?}",
12199            scene_delta.new_graph.objects
12200        );
12201
12202        ctx.close().await;
12203        mock_ctx.close().await;
12204    }
12205
12206    #[tokio::test(flavor = "multi_thread")]
12207    async fn test_diameter_single_arc_segment_with_label_position() {
12208        let initial_source = "\
12209sketch(on = XY) {
12210  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12211}
12212";
12213
12214        let program = Program::parse(initial_source).unwrap().0.unwrap();
12215        let mut frontend = FrontendState::new();
12216        let mock_ctx = ExecutorContext::new_mock(None).await;
12217        let version = Version(0);
12218
12219        frontend.program = program.clone();
12220        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12221        frontend.update_state_after_exec(outcome, true);
12222        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12223        let sketch_id = sketch_object.id;
12224        let sketch = expect_sketch(sketch_object);
12225        let arc_id = sketch
12226            .segments
12227            .iter()
12228            .find(|&seg_id| {
12229                let obj = frontend.scene_graph.objects.get(seg_id.0);
12230                matches!(
12231                    obj.map(|o| &o.kind),
12232                    Some(ObjectKind::Segment {
12233                        segment: Segment::Arc(_)
12234                    })
12235                )
12236            })
12237            .unwrap();
12238
12239        let label_position = Point2d {
12240            x: Number {
12241                value: 10.0,
12242                units: NumericSuffix::Mm,
12243            },
12244            y: Number {
12245                value: 11.0,
12246                units: NumericSuffix::Mm,
12247            },
12248        };
12249        let constraint = Constraint::Diameter(Diameter {
12250            arc: *arc_id,
12251            diameter: Number {
12252                value: 10.0,
12253                units: NumericSuffix::Mm,
12254            },
12255            label_position: Some(label_position.clone()),
12256            source: Default::default(),
12257        });
12258        let (src_delta, scene_delta) = frontend
12259            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12260            .await
12261            .unwrap();
12262        assert_eq!(
12263            src_delta.text.as_str(),
12264            "\
12265sketch(on = XY) {
12266  arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12267  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12268}
12269"
12270        );
12271
12272        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12273        let sketch = expect_sketch(sketch_object);
12274        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12275        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12276            panic!("Expected constraint object");
12277        };
12278        let Constraint::Diameter(diameter) = constraint else {
12279            panic!("Expected diameter constraint");
12280        };
12281        assert_eq!(diameter.label_position, Some(label_position));
12282
12283        mock_ctx.close().await;
12284    }
12285
12286    #[tokio::test(flavor = "multi_thread")]
12287    async fn test_edit_diameter_constraint_label_position() {
12288        let initial_source = "\
12289sketch(on = XY) {
12290  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12291  diameter(arc1) == 10mm
12292}
12293";
12294
12295        let program = Program::parse(initial_source).unwrap().0.unwrap();
12296        let mut frontend = FrontendState::new();
12297        let mock_ctx = ExecutorContext::new_mock(None).await;
12298        let version = Version(0);
12299
12300        frontend.program = program.clone();
12301        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12302        frontend.update_state_after_exec(outcome, true);
12303        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12304        let sketch_id = sketch_object.id;
12305        let sketch = expect_sketch(sketch_object);
12306        let constraint_id = sketch.constraints[0];
12307        let label_position = Point2d {
12308            x: Number {
12309                value: 10.0,
12310                units: NumericSuffix::Mm,
12311            },
12312            y: Number {
12313                value: 11.0,
12314                units: NumericSuffix::Mm,
12315            },
12316        };
12317
12318        let (src_delta, scene_delta) = frontend
12319            .edit_distance_constraint_label_position(
12320                &mock_ctx,
12321                version,
12322                sketch_id,
12323                constraint_id,
12324                label_position.clone(),
12325                vec![],
12326            )
12327            .await
12328            .unwrap();
12329        assert_eq!(
12330            src_delta.text.as_str(),
12331            "\
12332sketch(on = XY) {
12333  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12334  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12335}
12336"
12337        );
12338
12339        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12340        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12341            panic!("Expected constraint object");
12342        };
12343        let Constraint::Diameter(diameter) = constraint else {
12344            panic!("Expected diameter constraint");
12345        };
12346        assert_eq!(diameter.label_position, Some(label_position));
12347
12348        mock_ctx.close().await;
12349    }
12350
12351    #[tokio::test(flavor = "multi_thread")]
12352    async fn test_diameter_error_cases() {
12353        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12354        let mock_ctx = ExecutorContext::new_mock(None).await;
12355        let version = Version(0);
12356
12357        // Test: Single point should error
12358        let initial_source_point = "\
12359sketch(on = XY) {
12360  point(at = [var 1, var 2])
12361}
12362";
12363        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12364        let mut frontend_point = FrontendState::new();
12365        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12366        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12367        let sketch_id_point = sketch_object_point.id;
12368        let sketch_point = expect_sketch(sketch_object_point);
12369        let point_id = *sketch_point.segments.first().unwrap();
12370
12371        let constraint_point = Constraint::Diameter(Diameter {
12372            arc: point_id,
12373            diameter: Number {
12374                value: 10.0,
12375                units: NumericSuffix::Mm,
12376            },
12377            label_position: None,
12378            source: Default::default(),
12379        });
12380        let result_point = frontend_point
12381            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12382            .await;
12383        assert!(result_point.is_err(), "Single point should error for diameter");
12384
12385        // Test: Single line segment should error (only arc segments supported)
12386        let initial_source_line = "\
12387sketch(on = XY) {
12388  line(start = [var 1, var 2], end = [var 3, var 4])
12389}
12390";
12391        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12392        let mut frontend_line = FrontendState::new();
12393        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12394        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12395        let sketch_id_line = sketch_object_line.id;
12396        let sketch_line = expect_sketch(sketch_object_line);
12397        let line_id = *sketch_line.segments.first().unwrap();
12398
12399        let constraint_line = Constraint::Diameter(Diameter {
12400            arc: line_id,
12401            diameter: Number {
12402                value: 10.0,
12403                units: NumericSuffix::Mm,
12404            },
12405            label_position: None,
12406            source: Default::default(),
12407        });
12408        let result_line = frontend_line
12409            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12410            .await;
12411        assert!(result_line.is_err(), "Single line segment should error for diameter");
12412
12413        ctx.close().await;
12414        mock_ctx.close().await;
12415    }
12416
12417    #[tokio::test(flavor = "multi_thread")]
12418    async fn test_line_horizontal() {
12419        let initial_source = "\
12420sketch(on = XY) {
12421  line(start = [var 1, var 2], end = [var 3, var 4])
12422}
12423";
12424
12425        let program = Program::parse(initial_source).unwrap().0.unwrap();
12426
12427        let mut frontend = FrontendState::new();
12428
12429        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12430        let mock_ctx = ExecutorContext::new_mock(None).await;
12431        let version = Version(0);
12432
12433        frontend.hack_set_program(&ctx, program).await.unwrap();
12434        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12435        let sketch_id = sketch_object.id;
12436        let sketch = expect_sketch(sketch_object);
12437        let line1_id = *sketch.segments.get(2).unwrap();
12438
12439        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12440        let (src_delta, scene_delta) = frontend
12441            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12442            .await
12443            .unwrap();
12444        assert_eq!(
12445            src_delta.text.as_str(),
12446            "\
12447sketch(on = XY) {
12448  line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12449  horizontal(line1)
12450}
12451"
12452        );
12453        assert_eq!(
12454            scene_delta.new_graph.objects.len(),
12455            6,
12456            "{:#?}",
12457            scene_delta.new_graph.objects
12458        );
12459
12460        ctx.close().await;
12461        mock_ctx.close().await;
12462    }
12463
12464    #[tokio::test(flavor = "multi_thread")]
12465    async fn test_control_point_spline_edge_horizontal() {
12466        let initial_source = "\
12467@settings(experimentalFeatures = allow)
12468splineSketch = sketch(on = XY) {
12469  controlPointSpline1 = controlPointSpline(points = [
12470    [var 0mm, var 0mm],
12471    [var 10mm, var 20mm],
12472    [var 20mm, var 0mm],
12473  ])
12474}
12475";
12476
12477        let program = Program::parse(initial_source).unwrap().0.unwrap();
12478
12479        let mut frontend = FrontendState::new();
12480
12481        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12482        let mock_ctx = ExecutorContext::new_mock(None).await;
12483        let version = Version(0);
12484
12485        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12486        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12487        let sketch_id = sketch_object.id;
12488        let sketch = expect_sketch(sketch_object);
12489        let spline_id = sketch
12490            .segments
12491            .iter()
12492            .copied()
12493            .find(|seg_id| {
12494                matches!(
12495                    &frontend.scene_graph.objects[seg_id.0].kind,
12496                    ObjectKind::Segment {
12497                        segment: Segment::ControlPointSpline(_)
12498                    }
12499                )
12500            })
12501            .expect("Expected a control point spline segment in sketch");
12502        let edge_id = frontend
12503            .scene_graph
12504            .objects
12505            .iter()
12506            .find_map(|obj| match &obj.kind {
12507                ObjectKind::Segment {
12508                    segment: Segment::Line(line),
12509                } if line.owner == Some(spline_id) => Some(obj.id),
12510                _ => None,
12511            })
12512            .expect("Expected an owned control-polygon edge");
12513
12514        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12515        let (src_delta, _) = frontend
12516            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12517            .await
12518            .unwrap();
12519        assert!(
12520            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12521            "Expected horizontal constraint on spline edge, got: {}",
12522            src_delta.text
12523        );
12524
12525        ctx.close().await;
12526        mock_ctx.close().await;
12527    }
12528
12529    #[tokio::test(flavor = "multi_thread")]
12530    async fn test_control_point_spline_edge_angle() {
12531        let initial_source = "\
12532@settings(experimentalFeatures = allow)
12533splineSketch = sketch(on = XY) {
12534  controlPointSpline1 = controlPointSpline(points = [
12535    [var 0mm, var 0mm],
12536    [var 10mm, var 20mm],
12537    [var 20mm, var 0mm],
12538  ])
12539
12540  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12541}
12542";
12543
12544        let program = Program::parse(initial_source).unwrap().0.unwrap();
12545
12546        let mut frontend = FrontendState::new();
12547
12548        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12549        let mock_ctx = ExecutorContext::new_mock(None).await;
12550        let version = Version(0);
12551
12552        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12553        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12554        let sketch_id = sketch_object.id;
12555        let sketch = expect_sketch(sketch_object);
12556        let spline_id = sketch
12557            .segments
12558            .iter()
12559            .copied()
12560            .find(|seg_id| {
12561                matches!(
12562                    &frontend.scene_graph.objects[seg_id.0].kind,
12563                    ObjectKind::Segment {
12564                        segment: Segment::ControlPointSpline(_)
12565                    }
12566                )
12567            })
12568            .expect("Expected a control point spline segment in sketch");
12569        let edge_id = frontend
12570            .scene_graph
12571            .objects
12572            .iter()
12573            .find_map(|obj| match &obj.kind {
12574                ObjectKind::Segment {
12575                    segment: Segment::Line(line),
12576                } if line.owner == Some(spline_id) => Some(obj.id),
12577                _ => None,
12578            })
12579            .expect("Expected an owned control-polygon edge");
12580        let line1_id = frontend
12581            .scene_graph
12582            .objects
12583            .iter()
12584            .find_map(|obj| match &obj.kind {
12585                ObjectKind::Segment {
12586                    segment: Segment::Line(line),
12587                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12588                _ => None,
12589            })
12590            .or_else(|| {
12591                sketch.segments.iter().copied().find(|seg_id| {
12592                    matches!(
12593                        &frontend.scene_graph.objects[seg_id.0].kind,
12594                        ObjectKind::Segment {
12595                            segment: Segment::Line(line),
12596                        } if line.owner.is_none()
12597                    )
12598                })
12599            })
12600            .expect("Expected a standalone line segment in sketch");
12601
12602        let constraint = Constraint::Angle(Angle {
12603            lines: vec![line1_id, edge_id],
12604            angle: Number {
12605                value: 30.0,
12606                units: NumericSuffix::Deg,
12607            },
12608            source: Default::default(),
12609        });
12610        let (src_delta, _) = frontend
12611            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12612            .await
12613            .unwrap();
12614        assert!(
12615            src_delta
12616                .text
12617                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12618            "Expected angle constraint on spline edge, got: {}",
12619            src_delta.text
12620        );
12621
12622        ctx.close().await;
12623        mock_ctx.close().await;
12624    }
12625
12626    #[tokio::test(flavor = "multi_thread")]
12627    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12628        let initial_source = "\
12629@settings(experimentalFeatures = allow)
12630splineSketch = sketch(on = XY) {
12631  spline1 = controlPointSpline(points = [
12632    [var 0mm, var 0mm],
12633    [var 10mm, var 20mm],
12634    [var 20mm, var 0mm],
12635  ])
12636  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12637  coincident([spline1.controls[1], spline1.edges[0]])
12638  coincident([spline1.controls[0], line1])
12639}
12640";
12641
12642        let program = Program::parse(initial_source).unwrap().0.unwrap();
12643
12644        let mut frontend = FrontendState::new();
12645
12646        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12647        let mock_ctx = ExecutorContext::new_mock(None).await;
12648
12649        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12650
12651        let ui_scene_graph = frontend.scene_graph_for_ui();
12652        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12653        let sketch = expect_sketch(sketch_object);
12654
12655        assert_eq!(
12656            sketch.constraints.len(),
12657            1,
12658            "Expected only the external coincident constraint to remain visible in the UI scene graph"
12659        );
12660
12661        let visible_constraints = ui_scene_graph
12662            .objects
12663            .iter()
12664            .filter_map(|object| match &object.kind {
12665                ObjectKind::Constraint {
12666                    constraint: Constraint::Coincident(coincident),
12667                } => Some(coincident.clone()),
12668                _ => None,
12669            })
12670            .collect::<Vec<_>>();
12671
12672        assert_eq!(
12673            visible_constraints.len(),
12674            1,
12675            "Expected only one coincident constraint object in the UI scene graph"
12676        );
12677        assert_eq!(
12678            visible_constraints[0].get_segments().len(),
12679            2,
12680            "Expected the remaining visible coincident constraint to reference two segments"
12681        );
12682
12683        ctx.close().await;
12684        mock_ctx.close().await;
12685    }
12686
12687    #[tokio::test(flavor = "multi_thread")]
12688    async fn test_edit_control_point_spline_can_append_control_point() {
12689        let initial_source = "\
12690@settings(experimentalFeatures = allow)
12691splineSketch = sketch(on = XY) {
12692  controlPointSpline(points = [
12693    [var 0mm, var 0mm],
12694    [var 10mm, var 20mm],
12695    [var 20mm, var 0mm],
12696  ])
12697}
12698";
12699
12700        let program = Program::parse(initial_source).unwrap().0.unwrap();
12701
12702        let mut frontend = FrontendState::new();
12703
12704        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12705        let mock_ctx = ExecutorContext::new_mock(None).await;
12706        let version = Version(0);
12707
12708        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12709        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12710        let sketch_id = sketch_object.id;
12711        let sketch = expect_sketch(sketch_object);
12712        let spline_id = sketch
12713            .segments
12714            .iter()
12715            .copied()
12716            .find(|seg_id| {
12717                matches!(
12718                    &frontend.scene_graph.objects[seg_id.0].kind,
12719                    ObjectKind::Segment {
12720                        segment: Segment::ControlPointSpline(_)
12721                    }
12722                )
12723            })
12724            .expect("Expected a control point spline segment in sketch");
12725
12726        let ctor = ControlPointSplineCtor {
12727            points: vec![
12728                Point2d {
12729                    x: Expr::Var(Number {
12730                        value: 0.0,
12731                        units: NumericSuffix::Mm,
12732                    }),
12733                    y: Expr::Var(Number {
12734                        value: 0.0,
12735                        units: NumericSuffix::Mm,
12736                    }),
12737                },
12738                Point2d {
12739                    x: Expr::Var(Number {
12740                        value: 10.0,
12741                        units: NumericSuffix::Mm,
12742                    }),
12743                    y: Expr::Var(Number {
12744                        value: 20.0,
12745                        units: NumericSuffix::Mm,
12746                    }),
12747                },
12748                Point2d {
12749                    x: Expr::Var(Number {
12750                        value: 20.0,
12751                        units: NumericSuffix::Mm,
12752                    }),
12753                    y: Expr::Var(Number {
12754                        value: 0.0,
12755                        units: NumericSuffix::Mm,
12756                    }),
12757                },
12758                Point2d {
12759                    x: Expr::Var(Number {
12760                        value: 30.0,
12761                        units: NumericSuffix::Mm,
12762                    }),
12763                    y: Expr::Var(Number {
12764                        value: 10.0,
12765                        units: NumericSuffix::Mm,
12766                    }),
12767                },
12768            ],
12769            construction: None,
12770        };
12771
12772        let segments = vec![ExistingSegmentCtor {
12773            id: spline_id,
12774            ctor: SegmentCtor::ControlPointSpline(ctor),
12775        }];
12776        let (src_delta, scene_delta) = frontend
12777            .edit_segments(&mock_ctx, version, sketch_id, segments)
12778            .await
12779            .unwrap();
12780
12781        assert!(
12782            src_delta.text.contains("[var 30mm, var 10mm]"),
12783            "Expected appended spline control point in source, got: {}",
12784            src_delta.text
12785        );
12786
12787        assert!(
12788            scene_delta.invalidates_ids,
12789            "Expected appending a spline control point to invalidate ids"
12790        );
12791        let updated_spline = scene_delta
12792            .new_graph
12793            .objects
12794            .iter()
12795            .find_map(|obj| match &obj.kind {
12796                ObjectKind::Segment {
12797                    segment: Segment::ControlPointSpline(updated_spline),
12798                } if updated_spline.controls.len() == 4 => Some(updated_spline),
12799                _ => None,
12800            })
12801            .expect("Expected edited scene graph to contain a four-point control point spline");
12802        assert_eq!(
12803            updated_spline.controls.len(),
12804            4,
12805            "Expected edited spline to expose four control points"
12806        );
12807
12808        ctx.close().await;
12809        mock_ctx.close().await;
12810    }
12811
12812    #[tokio::test(flavor = "multi_thread")]
12813    async fn test_line_vertical() {
12814        let initial_source = "\
12815sketch(on = XY) {
12816  line(start = [var 1, var 2], end = [var 3, var 4])
12817}
12818";
12819
12820        let program = Program::parse(initial_source).unwrap().0.unwrap();
12821
12822        let mut frontend = FrontendState::new();
12823
12824        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12825        let mock_ctx = ExecutorContext::new_mock(None).await;
12826        let version = Version(0);
12827
12828        frontend.hack_set_program(&ctx, program).await.unwrap();
12829        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12830        let sketch_id = sketch_object.id;
12831        let sketch = expect_sketch(sketch_object);
12832        let line1_id = *sketch.segments.get(2).unwrap();
12833
12834        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12835        let (src_delta, scene_delta) = frontend
12836            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12837            .await
12838            .unwrap();
12839        assert_eq!(
12840            src_delta.text.as_str(),
12841            "\
12842sketch(on = XY) {
12843  line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12844  vertical(line1)
12845}
12846"
12847        );
12848        assert_eq!(
12849            scene_delta.new_graph.objects.len(),
12850            6,
12851            "{:#?}",
12852            scene_delta.new_graph.objects
12853        );
12854
12855        ctx.close().await;
12856        mock_ctx.close().await;
12857    }
12858
12859    #[tokio::test(flavor = "multi_thread")]
12860    async fn test_points_vertical() {
12861        let initial_source = "\
12862sketch001 = sketch(on = XY) {
12863  p0 = point(at = [var -2.23mm, var 3.1mm])
12864  pf = point(at = [4, 4])
12865}
12866";
12867
12868        let program = Program::parse(initial_source).unwrap().0.unwrap();
12869
12870        let mut frontend = FrontendState::new();
12871
12872        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12873        let mock_ctx = ExecutorContext::new_mock(None).await;
12874        let version = Version(0);
12875
12876        frontend.hack_set_program(&ctx, program).await.unwrap();
12877        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12878        let sketch_id = sketch_object.id;
12879        let sketch = expect_sketch(sketch_object);
12880        let point_ids = vec![
12881            sketch.segments.first().unwrap().to_owned(),
12882            sketch.segments.get(1).unwrap().to_owned(),
12883        ];
12884
12885        let constraint = Constraint::Vertical(Vertical::Points {
12886            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12887        });
12888        let (src_delta, scene_delta) = frontend
12889            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12890            .await
12891            .unwrap();
12892        assert_eq!(
12893            src_delta.text.as_str(),
12894            "\
12895sketch001 = sketch(on = XY) {
12896  p0 = point(at = [var 4mm, var 3.1mm])
12897  pf = point(at = [4, 4])
12898  vertical([p0, pf])
12899}
12900"
12901        );
12902        assert_eq!(
12903            scene_delta.new_graph.objects.len(),
12904            5,
12905            "{:#?}",
12906            scene_delta.new_graph.objects
12907        );
12908
12909        ctx.close().await;
12910        mock_ctx.close().await;
12911    }
12912
12913    #[tokio::test(flavor = "multi_thread")]
12914    async fn test_points_horizontal() {
12915        let initial_source = "\
12916sketch001 = sketch(on = XY) {
12917  p0 = point(at = [var -2.23mm, var 3.1mm])
12918  pf = point(at = [4, 4])
12919}
12920";
12921
12922        let program = Program::parse(initial_source).unwrap().0.unwrap();
12923
12924        let mut frontend = FrontendState::new();
12925
12926        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12927        let mock_ctx = ExecutorContext::new_mock(None).await;
12928        let version = Version(0);
12929
12930        frontend.hack_set_program(&ctx, program).await.unwrap();
12931        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12932        let sketch_id = sketch_object.id;
12933        let sketch = expect_sketch(sketch_object);
12934        let point_ids = vec![
12935            sketch.segments.first().unwrap().to_owned(),
12936            sketch.segments.get(1).unwrap().to_owned(),
12937        ];
12938
12939        let constraint = Constraint::Horizontal(Horizontal::Points {
12940            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12941        });
12942        let (src_delta, scene_delta) = frontend
12943            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12944            .await
12945            .unwrap();
12946        assert_eq!(
12947            src_delta.text.as_str(),
12948            "\
12949sketch001 = sketch(on = XY) {
12950  p0 = point(at = [var -2.23mm, var 4mm])
12951  pf = point(at = [4, 4])
12952  horizontal([p0, pf])
12953}
12954"
12955        );
12956        assert_eq!(
12957            scene_delta.new_graph.objects.len(),
12958            5,
12959            "{:#?}",
12960            scene_delta.new_graph.objects
12961        );
12962
12963        ctx.close().await;
12964        mock_ctx.close().await;
12965    }
12966
12967    #[tokio::test(flavor = "multi_thread")]
12968    async fn test_point_horizontal_with_origin() {
12969        let initial_source = "\
12970sketch001 = sketch(on = XY) {
12971  p0 = point(at = [var -2.23mm, var 3.1mm])
12972}
12973";
12974
12975        let program = Program::parse(initial_source).unwrap().0.unwrap();
12976
12977        let mut frontend = FrontendState::new();
12978
12979        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12980        let mock_ctx = ExecutorContext::new_mock(None).await;
12981        let version = Version(0);
12982
12983        frontend.hack_set_program(&ctx, program).await.unwrap();
12984        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12985        let sketch_id = sketch_object.id;
12986        let sketch = expect_sketch(sketch_object);
12987        let point_id = *sketch.segments.first().unwrap();
12988
12989        let constraint = Constraint::Horizontal(Horizontal::Points {
12990            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12991        });
12992        let (src_delta, scene_delta) = frontend
12993            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12994            .await
12995            .unwrap();
12996        assert_eq!(
12997            src_delta.text.as_str(),
12998            "\
12999sketch001 = sketch(on = XY) {
13000  p0 = point(at = [var -2.23mm, var 0mm])
13001  horizontal([p0, ORIGIN])
13002}
13003"
13004        );
13005        assert_eq!(
13006            scene_delta.new_graph.objects.len(),
13007            4,
13008            "{:#?}",
13009            scene_delta.new_graph.objects
13010        );
13011
13012        ctx.close().await;
13013        mock_ctx.close().await;
13014    }
13015
13016    #[tokio::test(flavor = "multi_thread")]
13017    async fn test_lines_equal_length() {
13018        let initial_source = "\
13019sketch(on = XY) {
13020  line(start = [var 1, var 2], end = [var 3, var 4])
13021  line(start = [var 5, var 6], end = [var 7, var 8])
13022}
13023";
13024
13025        let program = Program::parse(initial_source).unwrap().0.unwrap();
13026
13027        let mut frontend = FrontendState::new();
13028
13029        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13030        let mock_ctx = ExecutorContext::new_mock(None).await;
13031        let version = Version(0);
13032
13033        frontend.hack_set_program(&ctx, program).await.unwrap();
13034        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13035        let sketch_id = sketch_object.id;
13036        let sketch = expect_sketch(sketch_object);
13037        let line1_id = *sketch.segments.get(2).unwrap();
13038        let line2_id = *sketch.segments.get(5).unwrap();
13039
13040        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13041            lines: vec![line1_id, line2_id],
13042        });
13043        let (src_delta, scene_delta) = frontend
13044            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13045            .await
13046            .unwrap();
13047        assert_eq!(
13048            src_delta.text.as_str(),
13049            "\
13050sketch(on = XY) {
13051  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13052  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13053  equalLength([line1, line2])
13054}
13055"
13056        );
13057        assert_eq!(
13058            scene_delta.new_graph.objects.len(),
13059            9,
13060            "{:#?}",
13061            scene_delta.new_graph.objects
13062        );
13063
13064        ctx.close().await;
13065        mock_ctx.close().await;
13066    }
13067
13068    #[tokio::test(flavor = "multi_thread")]
13069    async fn test_add_constraint_multi_line_equal_length() {
13070        let initial_source = "\
13071sketch(on = XY) {
13072  line(start = [var 1, var 2], end = [var 3, var 4])
13073  line(start = [var 5, var 6], end = [var 7, var 8])
13074  line(start = [var 9, var 10], end = [var 11, var 12])
13075}
13076";
13077
13078        let program = Program::parse(initial_source).unwrap().0.unwrap();
13079
13080        let mut frontend = FrontendState::new();
13081        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13082        let mock_ctx = ExecutorContext::new_mock(None).await;
13083        let version = Version(0);
13084
13085        frontend.hack_set_program(&ctx, program).await.unwrap();
13086        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13087        let sketch_id = sketch_object.id;
13088        let sketch = expect_sketch(sketch_object);
13089        let line1_id = *sketch.segments.get(2).unwrap();
13090        let line2_id = *sketch.segments.get(5).unwrap();
13091        let line3_id = *sketch.segments.get(8).unwrap();
13092
13093        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13094            lines: vec![line1_id, line2_id, line3_id],
13095        });
13096        let (src_delta, scene_delta) = frontend
13097            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13098            .await
13099            .unwrap();
13100        assert_eq!(
13101            src_delta.text.as_str(),
13102            "\
13103sketch(on = XY) {
13104  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13105  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13106  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13107  equalLength([line1, line2, line3])
13108}
13109"
13110        );
13111        let constraints = scene_delta
13112            .new_graph
13113            .objects
13114            .iter()
13115            .filter_map(|obj| {
13116                let ObjectKind::Constraint { constraint } = &obj.kind else {
13117                    return None;
13118                };
13119                Some(constraint)
13120            })
13121            .collect::<Vec<_>>();
13122
13123        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13124        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13125            panic!("expected equal length constraint, got {:?}", constraints[0]);
13126        };
13127        assert_eq!(lines_equal_length.lines.len(), 3);
13128
13129        ctx.close().await;
13130        mock_ctx.close().await;
13131    }
13132
13133    #[tokio::test(flavor = "multi_thread")]
13134    async fn test_lines_parallel() {
13135        let initial_source = "\
13136sketch(on = XY) {
13137  line(start = [var 1, var 2], end = [var 3, var 4])
13138  line(start = [var 5, var 6], end = [var 7, var 8])
13139}
13140";
13141
13142        let program = Program::parse(initial_source).unwrap().0.unwrap();
13143
13144        let mut frontend = FrontendState::new();
13145
13146        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13147        let mock_ctx = ExecutorContext::new_mock(None).await;
13148        let version = Version(0);
13149
13150        frontend.hack_set_program(&ctx, program).await.unwrap();
13151        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13152        let sketch_id = sketch_object.id;
13153        let sketch = expect_sketch(sketch_object);
13154        let line1_id = *sketch.segments.get(2).unwrap();
13155        let line2_id = *sketch.segments.get(5).unwrap();
13156
13157        let constraint = Constraint::Parallel(Parallel {
13158            lines: vec![line1_id, line2_id],
13159        });
13160        let (src_delta, scene_delta) = frontend
13161            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13162            .await
13163            .unwrap();
13164        assert_eq!(
13165            src_delta.text.as_str(),
13166            "\
13167sketch(on = XY) {
13168  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13169  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13170  parallel([line1, line2])
13171}
13172"
13173        );
13174        assert_eq!(
13175            scene_delta.new_graph.objects.len(),
13176            9,
13177            "{:#?}",
13178            scene_delta.new_graph.objects
13179        );
13180
13181        ctx.close().await;
13182        mock_ctx.close().await;
13183    }
13184
13185    #[tokio::test(flavor = "multi_thread")]
13186    async fn test_lines_parallel_multiline() {
13187        let initial_source = "\
13188sketch(on = XY) {
13189  line(start = [var 1, var 2], end = [var 3, var 4])
13190  line(start = [var 5, var 6], end = [var 7, var 8])
13191  line(start = [var 9, var 10], end = [var 11, var 12])
13192}
13193";
13194
13195        let program = Program::parse(initial_source).unwrap().0.unwrap();
13196
13197        let mut frontend = FrontendState::new();
13198
13199        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13200        let mock_ctx = ExecutorContext::new_mock(None).await;
13201        let version = Version(0);
13202
13203        frontend.hack_set_program(&ctx, program).await.unwrap();
13204        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13205        let sketch_id = sketch_object.id;
13206        let sketch = expect_sketch(sketch_object);
13207        let line1_id = *sketch.segments.get(2).unwrap();
13208        let line2_id = *sketch.segments.get(5).unwrap();
13209        let line3_id = *sketch.segments.get(8).unwrap();
13210
13211        let constraint = Constraint::Parallel(Parallel {
13212            lines: vec![line1_id, line2_id, line3_id],
13213        });
13214        let (src_delta, scene_delta) = frontend
13215            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13216            .await
13217            .unwrap();
13218        assert_eq!(
13219            src_delta.text.as_str(),
13220            "\
13221sketch(on = XY) {
13222  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13223  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13224  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13225  parallel([line1, line2, line3])
13226}
13227"
13228        );
13229
13230        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13231        let sketch = expect_sketch(sketch_object);
13232        assert_eq!(sketch.constraints.len(), 1);
13233
13234        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13235        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13236            panic!("Expected constraint object");
13237        };
13238        let Constraint::Parallel(parallel) = constraint else {
13239            panic!("Expected parallel constraint");
13240        };
13241        assert_eq!(parallel.lines.len(), 3);
13242
13243        ctx.close().await;
13244        mock_ctx.close().await;
13245    }
13246
13247    #[tokio::test(flavor = "multi_thread")]
13248    async fn test_lines_perpendicular() {
13249        let initial_source = "\
13250sketch(on = XY) {
13251  line(start = [var 1, var 2], end = [var 3, var 4])
13252  line(start = [var 5, var 6], end = [var 7, var 8])
13253}
13254";
13255
13256        let program = Program::parse(initial_source).unwrap().0.unwrap();
13257
13258        let mut frontend = FrontendState::new();
13259
13260        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13261        let mock_ctx = ExecutorContext::new_mock(None).await;
13262        let version = Version(0);
13263
13264        frontend.hack_set_program(&ctx, program).await.unwrap();
13265        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13266        let sketch_id = sketch_object.id;
13267        let sketch = expect_sketch(sketch_object);
13268        let line1_id = *sketch.segments.get(2).unwrap();
13269        let line2_id = *sketch.segments.get(5).unwrap();
13270
13271        let constraint = Constraint::Perpendicular(Perpendicular {
13272            lines: vec![line1_id, line2_id],
13273        });
13274        let (src_delta, scene_delta) = frontend
13275            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13276            .await
13277            .unwrap();
13278        assert_eq!(
13279            src_delta.text.as_str(),
13280            "\
13281sketch(on = XY) {
13282  line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13283  line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13284  perpendicular([line1, line2])
13285}
13286"
13287        );
13288        assert_eq!(
13289            scene_delta.new_graph.objects.len(),
13290            9,
13291            "{:#?}",
13292            scene_delta.new_graph.objects
13293        );
13294
13295        ctx.close().await;
13296        mock_ctx.close().await;
13297    }
13298
13299    #[tokio::test(flavor = "multi_thread")]
13300    async fn test_lines_angle() {
13301        let initial_source = "\
13302sketch(on = XY) {
13303  line(start = [var 1, var 2], end = [var 3, var 4])
13304  line(start = [var 5, var 6], end = [var 7, var 8])
13305}
13306";
13307
13308        let program = Program::parse(initial_source).unwrap().0.unwrap();
13309
13310        let mut frontend = FrontendState::new();
13311
13312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13313        let mock_ctx = ExecutorContext::new_mock(None).await;
13314        let version = Version(0);
13315
13316        frontend.hack_set_program(&ctx, program).await.unwrap();
13317        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13318        let sketch_id = sketch_object.id;
13319        let sketch = expect_sketch(sketch_object);
13320        let line1_id = *sketch.segments.get(2).unwrap();
13321        let line2_id = *sketch.segments.get(5).unwrap();
13322
13323        let constraint = Constraint::Angle(Angle {
13324            lines: vec![line1_id, line2_id],
13325            angle: Number {
13326                value: 30.0,
13327                units: NumericSuffix::Deg,
13328            },
13329            source: Default::default(),
13330        });
13331        let (src_delta, scene_delta) = frontend
13332            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13333            .await
13334            .unwrap();
13335        assert_eq!(
13336            src_delta.text.as_str(),
13337            // The lack indentation is a formatter bug.
13338            "\
13339sketch(on = XY) {
13340  line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13341  line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13342  angle([line1, line2]) == 30deg
13343}
13344"
13345        );
13346        assert_eq!(
13347            scene_delta.new_graph.objects.len(),
13348            9,
13349            "{:#?}",
13350            scene_delta.new_graph.objects
13351        );
13352
13353        ctx.close().await;
13354        mock_ctx.close().await;
13355    }
13356
13357    #[tokio::test(flavor = "multi_thread")]
13358    async fn test_segments_tangent() {
13359        let initial_source = "\
13360sketch(on = XY) {
13361  line(start = [var 1, var 2], end = [var 3, var 4])
13362  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13363}
13364";
13365
13366        let program = Program::parse(initial_source).unwrap().0.unwrap();
13367
13368        let mut frontend = FrontendState::new();
13369
13370        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13371        let mock_ctx = ExecutorContext::new_mock(None).await;
13372        let version = Version(0);
13373
13374        frontend.hack_set_program(&ctx, program).await.unwrap();
13375        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13376        let sketch_id = sketch_object.id;
13377        let sketch = expect_sketch(sketch_object);
13378        let line1_id = *sketch.segments.get(2).unwrap();
13379        let arc1_id = *sketch.segments.get(6).unwrap();
13380
13381        let constraint = Constraint::Tangent(Tangent {
13382            input: vec![line1_id, arc1_id],
13383        });
13384        let (src_delta, scene_delta) = frontend
13385            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13386            .await
13387            .unwrap();
13388        assert_eq!(
13389            src_delta.text.as_str(),
13390            "\
13391sketch(on = XY) {
13392  line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13393  arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13394  tangent([line1, arc1])
13395}
13396"
13397        );
13398        assert_eq!(
13399            scene_delta.new_graph.objects.len(),
13400            10,
13401            "{:#?}",
13402            scene_delta.new_graph.objects
13403        );
13404
13405        ctx.close().await;
13406        mock_ctx.close().await;
13407    }
13408
13409    #[tokio::test(flavor = "multi_thread")]
13410    async fn test_point_midpoint() {
13411        let initial_source = "\
13412sketch(on = XY) {
13413  point(at = [var 1, var 1])
13414  line(start = [var 0, var 0], end = [var 6, var 4])
13415}
13416";
13417
13418        let program = Program::parse(initial_source).unwrap().0.unwrap();
13419
13420        let mut frontend = FrontendState::new();
13421
13422        let ctx = ExecutorContext::new_mock(None).await;
13423        let version = Version(0);
13424
13425        frontend.program = program.clone();
13426        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13427        frontend.update_state_after_exec(outcome, true);
13428        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13429        let sketch_id = sketch_object.id;
13430        let sketch = expect_sketch(sketch_object);
13431        let point_id = *sketch.segments.first().unwrap();
13432        let line_id = *sketch.segments.get(3).unwrap();
13433
13434        let constraint = Constraint::Midpoint(Midpoint {
13435            point: ConstraintSegment::from(point_id),
13436            segment: line_id,
13437        });
13438        let (src_delta, scene_delta) = frontend
13439            .add_constraint(&ctx, version, sketch_id, constraint)
13440            .await
13441            .unwrap();
13442        assert_eq!(
13443            src_delta.text.as_str(),
13444            "\
13445sketch(on = XY) {
13446  point1 = point(at = [var 2.33, var 1.67])
13447  line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13448  midpoint(line1, point = point1)
13449}
13450"
13451        );
13452        assert_eq!(
13453            scene_delta.new_graph.objects.len(),
13454            7,
13455            "{:#?}",
13456            scene_delta.new_graph.objects
13457        );
13458
13459        ctx.close().await;
13460    }
13461
13462    #[tokio::test(flavor = "multi_thread")]
13463    async fn test_segments_symmetric() {
13464        let initial_source = "\
13465sketch(on = XY) {
13466  line(start = [var 0, var 0], end = [var 0, var 4])
13467  line(start = [var 4, var 0], end = [var 4, var 4])
13468  line(start = [var 2, var -1], end = [var 2, var 5])
13469}
13470";
13471
13472        let program = Program::parse(initial_source).unwrap().0.unwrap();
13473
13474        let mut frontend = FrontendState::new();
13475
13476        let ctx = ExecutorContext::new_mock(None).await;
13477        let version = Version(0);
13478
13479        frontend.program = program.clone();
13480        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13481        frontend.update_state_after_exec(outcome, true);
13482        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13483        let sketch_id = sketch_object.id;
13484        let sketch = expect_sketch(sketch_object);
13485        let line1_id = *sketch.segments.get(2).unwrap();
13486        let line2_id = *sketch.segments.get(5).unwrap();
13487        let axis_id = *sketch.segments.get(8).unwrap();
13488
13489        let constraint = Constraint::Symmetric(Symmetric {
13490            input: vec![line1_id, line2_id],
13491            axis: axis_id,
13492        });
13493        let (src_delta, scene_delta) = frontend
13494            .add_constraint(&ctx, version, sketch_id, constraint)
13495            .await
13496            .unwrap();
13497        assert_eq!(
13498            src_delta.text.as_str(),
13499            "\
13500sketch(on = XY) {
13501  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13502  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13503  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13504  symmetric([line1, line2], axis = line3)
13505}
13506"
13507        );
13508        assert_eq!(
13509            scene_delta.new_graph.objects.len(),
13510            12,
13511            "{:#?}",
13512            scene_delta.new_graph.objects
13513        );
13514
13515        ctx.close().await;
13516    }
13517
13518    #[tokio::test(flavor = "multi_thread")]
13519    async fn test_point_arc_midpoint() {
13520        let initial_source = "\
13521sketch(on = XY) {
13522  point(at = [var 6, var 3])
13523  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13524}
13525";
13526
13527        let program = Program::parse(initial_source).unwrap().0.unwrap();
13528
13529        let mut frontend = FrontendState::new();
13530
13531        let ctx = ExecutorContext::new_mock(None).await;
13532        let version = Version(0);
13533
13534        frontend.program = program.clone();
13535        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13536        frontend.update_state_after_exec(outcome, true);
13537        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13538        let sketch_id = sketch_object.id;
13539        let sketch = expect_sketch(sketch_object);
13540        let point_id = *sketch.segments.first().unwrap();
13541        let arc_id = *sketch.segments.get(4).unwrap();
13542
13543        let constraint = Constraint::Midpoint(Midpoint {
13544            point: ConstraintSegment::from(point_id),
13545            segment: arc_id,
13546        });
13547        let (src_delta, scene_delta) = frontend
13548            .add_constraint(&ctx, version, sketch_id, constraint)
13549            .await
13550            .unwrap();
13551        assert_eq!(
13552            src_delta.text.as_str(),
13553            "\
13554sketch(on = XY) {
13555  point1 = point(at = [var 6, var 2.35])
13556  arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13557  midpoint(arc1, point = point1)
13558}
13559"
13560        );
13561        assert_eq!(
13562            scene_delta.new_graph.objects.len(),
13563            8,
13564            "{:#?}",
13565            scene_delta.new_graph.objects
13566        );
13567
13568        ctx.close().await;
13569    }
13570
13571    #[tokio::test(flavor = "multi_thread")]
13572    async fn test_origin_line_midpoint() {
13573        let initial_source = "\
13574sketch(on = XY) {
13575  line(start = [var 0, var 0], end = [var 6, var 4])
13576}
13577";
13578
13579        let program = Program::parse(initial_source).unwrap().0.unwrap();
13580
13581        let mut frontend = FrontendState::new();
13582
13583        let ctx = ExecutorContext::new_mock(None).await;
13584        let version = Version(0);
13585
13586        frontend.program = program.clone();
13587        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13588        frontend.update_state_after_exec(outcome, true);
13589        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13590        let sketch_id = sketch_object.id;
13591        let sketch = expect_sketch(sketch_object);
13592        let line_id = *sketch.segments.get(2).unwrap();
13593
13594        let constraint = Constraint::Midpoint(Midpoint {
13595            point: ConstraintSegment::ORIGIN,
13596            segment: line_id,
13597        });
13598        let (src_delta, scene_delta) = frontend
13599            .add_constraint(&ctx, version, sketch_id, constraint)
13600            .await
13601            .unwrap();
13602        assert_eq!(
13603            src_delta.text.as_str(),
13604            "\
13605sketch(on = XY) {
13606  line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13607  midpoint(line1, point = ORIGIN)
13608}
13609"
13610        );
13611        assert_eq!(
13612            scene_delta.new_graph.objects.len(),
13613            6,
13614            "{:#?}",
13615            scene_delta.new_graph.objects
13616        );
13617
13618        ctx.close().await;
13619    }
13620
13621    #[tokio::test(flavor = "multi_thread")]
13622    async fn test_origin_arc_midpoint() {
13623        let initial_source = "\
13624sketch(on = XY) {
13625  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13626}
13627";
13628
13629        let program = Program::parse(initial_source).unwrap().0.unwrap();
13630
13631        let mut frontend = FrontendState::new();
13632
13633        let ctx = ExecutorContext::new_mock(None).await;
13634        let version = Version(0);
13635
13636        frontend.program = program.clone();
13637        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13638        frontend.update_state_after_exec(outcome, true);
13639        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13640        let sketch_id = sketch_object.id;
13641        let sketch = expect_sketch(sketch_object);
13642        let arc_id = *sketch.segments.get(3).unwrap();
13643
13644        let constraint = Constraint::Midpoint(Midpoint {
13645            point: ConstraintSegment::ORIGIN,
13646            segment: arc_id,
13647        });
13648        let (src_delta, scene_delta) = frontend
13649            .add_constraint(&ctx, version, sketch_id, constraint)
13650            .await
13651            .unwrap();
13652        assert_eq!(
13653            src_delta.text.as_str(),
13654            "\
13655sketch(on = XY) {
13656  arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13657  midpoint(arc1, point = ORIGIN)
13658}
13659"
13660        );
13661        assert_eq!(
13662            scene_delta.new_graph.objects.len(),
13663            7,
13664            "{:#?}",
13665            scene_delta.new_graph.objects
13666        );
13667
13668        ctx.close().await;
13669    }
13670
13671    #[tokio::test(flavor = "multi_thread")]
13672    async fn test_segments_symmetric_arcs() {
13673        let initial_source = "\
13674sketch(on = XY) {
13675  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13676  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13677  line(start = [var 0, var -10], end = [var 0, var 10])
13678}
13679";
13680
13681        let program = Program::parse(initial_source).unwrap().0.unwrap();
13682
13683        let mut frontend = FrontendState::new();
13684
13685        let ctx = ExecutorContext::new_mock(None).await;
13686        let version = Version(0);
13687
13688        frontend.program = program.clone();
13689        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13690        frontend.update_state_after_exec(outcome, true);
13691        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13692        let sketch_id = sketch_object.id;
13693        let sketch = expect_sketch(sketch_object);
13694        let arc1_id = *sketch.segments.get(3).unwrap();
13695        let arc2_id = *sketch.segments.get(7).unwrap();
13696        let axis_id = *sketch.segments.get(10).unwrap();
13697
13698        let constraint = Constraint::Symmetric(Symmetric {
13699            input: vec![arc1_id, arc2_id],
13700            axis: axis_id,
13701        });
13702        let (src_delta, scene_delta) = frontend
13703            .add_constraint(&ctx, version, sketch_id, constraint)
13704            .await
13705            .unwrap();
13706        assert_eq!(
13707            src_delta.text.as_str(),
13708            "\
13709sketch(on = XY) {
13710  arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13711  arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13712  line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13713  symmetric([arc1, arc2], axis = line1)
13714}
13715"
13716        );
13717        assert_eq!(
13718            scene_delta.new_graph.objects.len(),
13719            14,
13720            "{:#?}",
13721            scene_delta.new_graph.objects
13722        );
13723
13724        ctx.close().await;
13725    }
13726
13727    #[tokio::test(flavor = "multi_thread")]
13728    async fn test_sketch_on_face_simple() {
13729        let initial_source = "\
13730len = 2mm
13731cube = startSketchOn(XY)
13732  |> startProfile(at = [0, 0])
13733  |> line(end = [len, 0], tag = $side)
13734  |> line(end = [0, len])
13735  |> line(end = [-len, 0])
13736  |> line(end = [0, -len])
13737  |> close()
13738  |> extrude(length = len)
13739
13740face = faceOf(cube, face = side)
13741";
13742
13743        let program = Program::parse(initial_source).unwrap().0.unwrap();
13744
13745        let mut frontend = FrontendState::new();
13746
13747        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13748        let mock_ctx = ExecutorContext::new_mock(None).await;
13749        let version = Version(0);
13750
13751        frontend.hack_set_program(&ctx, program).await.unwrap();
13752        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13753        let face_id = face_object.id;
13754
13755        let sketch_args = SketchCtor {
13756            on: Plane::Object(face_id),
13757        };
13758        let (_src_delta, scene_delta, sketch_id) = frontend
13759            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13760            .await
13761            .unwrap();
13762        assert_eq!(sketch_id, ObjectId(2));
13763        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13764        let sketch_object = &scene_delta.new_graph.objects[2];
13765        assert_eq!(sketch_object.id, ObjectId(2));
13766        assert_eq!(
13767            sketch_object.kind,
13768            ObjectKind::Sketch(Sketch {
13769                args: SketchCtor {
13770                    on: Plane::Object(face_id),
13771                },
13772                plane: face_id,
13773                segments: vec![],
13774                constraints: vec![],
13775            })
13776        );
13777        assert_eq!(scene_delta.new_graph.objects.len(), 8);
13778
13779        ctx.close().await;
13780        mock_ctx.close().await;
13781    }
13782
13783    #[tokio::test(flavor = "multi_thread")]
13784    async fn test_sketch_on_wall_artifact_from_region_extrude() {
13785        let initial_source = "\
13786s = sketch(on = YZ) {
13787  line1 = line(start = [0, 0], end = [0, 1])
13788  line2 = line(start = [0, 1], end = [1, 1])
13789  line3 = line(start = [1, 1], end = [0, 0])
13790}
13791region001 = region(point = [0.1, 0.1], sketch = s)
13792extrude001 = extrude(region001, length = 5)
13793";
13794
13795        let program = Program::parse(initial_source).unwrap().0.unwrap();
13796
13797        let mut frontend = FrontendState::new();
13798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13799        let version = Version(0);
13800
13801        frontend.hack_set_program(&ctx, program).await.unwrap();
13802        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13803
13804        let sketch_args = SketchCtor {
13805            on: Plane::Object(wall_object_id),
13806        };
13807        let (src_delta, _scene_delta, _sketch_id) = frontend
13808            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13809            .await
13810            .unwrap();
13811        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13812
13813        ctx.close().await;
13814    }
13815
13816    #[tokio::test(flavor = "multi_thread")]
13817    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13818        let initial_source = "\
13819sketch001 = sketch(on = YZ) {
13820  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13821  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13822  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13823  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13824  coincident([line1.end, line2.start])
13825  coincident([line2.end, line3.start])
13826  coincident([line3.end, line4.start])
13827  coincident([line4.end, line1.start])
13828  parallel([line2, line4])
13829  parallel([line3, line1])
13830  perpendicular([line1, line2])
13831  horizontal(line3)
13832  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13833}
13834region001 = region(point = [3.1, 3.74], sketch = sketch001)
13835extrude001 = extrude(region001, length = 5)
13836";
13837
13838        let program = Program::parse(initial_source).unwrap().0.unwrap();
13839
13840        let mut frontend = FrontendState::new();
13841        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13842        let version = Version(0);
13843
13844        frontend.hack_set_program(&ctx, program).await.unwrap();
13845        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13846
13847        let sketch_args = SketchCtor {
13848            on: Plane::Object(wall_object_id),
13849        };
13850        let (src_delta, _scene_delta, _sketch_id) = frontend
13851            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13852            .await
13853            .unwrap();
13854        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13855
13856        ctx.close().await;
13857    }
13858
13859    #[test]
13860    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13861        let source = "\
13862sketch001 = sketch(on = XY) {
13863  line(start = [0, 0], end = [1, 0])
13864}
13865part = subtract(boxSolid, tools = [cutSolid])
13866  |> appearance(color = \"#8f96a3\")
13867";
13868        let ast = Program::parse(source).unwrap().0.unwrap().ast;
13869        let line_start = source.find("line").unwrap();
13870        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13871        let line_ref = SourceRef::Simple {
13872            range: [line_start, line_end, 0].into(),
13873            node_path: None,
13874        };
13875        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13876
13877        let subtract_start = source.find("subtract").unwrap();
13878        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13879        let subtract_ref = SourceRef::Simple {
13880            range: [subtract_start, subtract_end, 0].into(),
13881            node_path: None,
13882        };
13883        assert_eq!(
13884            variable_name_containing_source_ref(&ast, &subtract_ref),
13885            Some("part".to_owned())
13886        );
13887    }
13888
13889    #[tokio::test(flavor = "multi_thread")]
13890    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13891        clear_mem_cache().await;
13892        let source = "\
13893boxSolid = startSketchOn(XY)
13894  |> startProfile(at = [0, 0])
13895  |> line(end = [4, 0], tag = $bottomEdge)
13896  |> line(end = [0, 4])
13897  |> line(end = [-4, 0])
13898  |> close()
13899  |> extrude(length = 10)
13900cutSolid = startSketchOn(XY)
13901  |> startProfile(at = [1, 1])
13902  |> line(end = [1, 0])
13903  |> line(end = [0, 1])
13904  |> line(end = [-1, 0])
13905  |> close()
13906  |> extrude(length = 10)
13907part = subtract(boxSolid, tools = [cutSolid])
13908  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13909";
13910        let program = Program::parse(source).unwrap().0.unwrap();
13911        let mut frontend = FrontendState::new();
13912        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13913        match frontend.hack_set_program(&ctx, program).await.unwrap() {
13914            SetProgramOutcome::Success { .. } => {}
13915            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13916        }
13917
13918        let sweep_call_start = source.find("extrude").unwrap();
13919        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13920        let part_call_start = source.find("subtract").unwrap();
13921        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13922        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13923        let composite_range = [part_call_start, part_call_end, 0].into();
13924
13925        let cap_object = frontend
13926            .scene_graph
13927            .objects
13928            .iter()
13929            .find(|object| {
13930                matches!(
13931                    object.kind,
13932                    ObjectKind::Cap(crate::frontend::api::Cap {
13933                        kind: crate::frontend::api::CapKind::End,
13934                        ..
13935                    })
13936                ) && matches!(
13937                    &object.source,
13938                    SourceRef::BackTrace { ranges }
13939                        if ranges.len() == 2 && ranges[0].0 == composite_range && ranges[1].0 == sweep_range
13940                )
13941            })
13942            .expect("expected end cap object to trace through subtract and original extrude");
13943
13944        let mut ast = frontend.program.ast.clone();
13945        let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13946        let cap_face_decl = ast::VariableDeclaration::new(
13947            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13948            ast::ItemVisibility::Default,
13949            ast::VariableKind::Const,
13950        );
13951        ast.body
13952            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13953                cap_face_decl,
13954            ))));
13955        let generated_source = source_from_ast(&ast);
13956
13957        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13958        assert!(!generated_source.contains("faceOf(boxSolid"));
13959        let ast::Expr::CallExpressionKw(call) = cap_expr else {
13960            panic!("expected faceOf call");
13961        };
13962        assert_eq!(call.callee.name.name, "faceOf");
13963        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13964            panic!("expected solid name");
13965        };
13966        assert_eq!(solid_name.name.name, "part");
13967        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13968            panic!("expected face name");
13969        };
13970        assert_eq!(face_name.name.name, "END");
13971
13972        ctx.close().await;
13973    }
13974
13975    #[tokio::test(flavor = "multi_thread")]
13976    async fn test_sketch_on_plane_incremental() {
13977        let initial_source = "\
13978len = 2mm
13979cube = startSketchOn(XY)
13980  |> startProfile(at = [0, 0])
13981  |> line(end = [len, 0], tag = $side)
13982  |> line(end = [0, len])
13983  |> line(end = [-len, 0])
13984  |> line(end = [0, -len])
13985  |> close()
13986  |> extrude(length = len)
13987
13988plane = planeOf(cube, face = side)
13989";
13990
13991        let program = Program::parse(initial_source).unwrap().0.unwrap();
13992
13993        let mut frontend = FrontendState::new();
13994
13995        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13996        let mock_ctx = ExecutorContext::new_mock(None).await;
13997        let version = Version(0);
13998
13999        frontend.hack_set_program(&ctx, program).await.unwrap();
14000        // Find the last plane since the first plane is the XY plane.
14001        let plane_object = frontend
14002            .scene_graph
14003            .objects
14004            .iter()
14005            .rev()
14006            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14007            .unwrap();
14008        let plane_id = plane_object.id;
14009
14010        let sketch_args = SketchCtor {
14011            on: Plane::Object(plane_id),
14012        };
14013        let (src_delta, scene_delta, sketch_id) = frontend
14014            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14015            .await
14016            .unwrap();
14017        assert_eq!(
14018            src_delta.text.as_str(),
14019            "\
14020len = 2mm
14021cube = startSketchOn(XY)
14022  |> startProfile(at = [0, 0])
14023  |> line(end = [len, 0], tag = $side)
14024  |> line(end = [0, len])
14025  |> line(end = [-len, 0])
14026  |> line(end = [0, -len])
14027  |> close()
14028  |> extrude(length = len)
14029
14030plane = planeOf(cube, face = side)
14031sketch001 = sketch(on = plane) {
14032}
14033"
14034        );
14035        assert_eq!(sketch_id, ObjectId(2));
14036        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14037        let sketch_object = &scene_delta.new_graph.objects[2];
14038        assert_eq!(sketch_object.id, ObjectId(2));
14039        assert_eq!(
14040            sketch_object.kind,
14041            ObjectKind::Sketch(Sketch {
14042                args: SketchCtor {
14043                    on: Plane::Object(plane_id),
14044                },
14045                plane: plane_id,
14046                segments: vec![],
14047                constraints: vec![],
14048            })
14049        );
14050        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14051
14052        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14053        assert_eq!(plane_object.id, plane_id);
14054        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14055
14056        ctx.close().await;
14057        mock_ctx.close().await;
14058    }
14059
14060    #[tokio::test(flavor = "multi_thread")]
14061    async fn test_new_sketch_uses_unique_variable_name() {
14062        let initial_source = "\
14063sketch1 = sketch(on = XY) {
14064}
14065";
14066
14067        let program = Program::parse(initial_source).unwrap().0.unwrap();
14068
14069        let mut frontend = FrontendState::new();
14070        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14071        let version = Version(0);
14072
14073        frontend.hack_set_program(&ctx, program).await.unwrap();
14074
14075        let sketch_args = SketchCtor {
14076            on: Plane::Default(PlaneName::Yz),
14077        };
14078        let (src_delta, _, _) = frontend
14079            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14080            .await
14081            .unwrap();
14082
14083        assert_eq!(
14084            src_delta.text.as_str(),
14085            "\
14086sketch1 = sketch(on = XY) {
14087}
14088sketch001 = sketch(on = YZ) {
14089}
14090"
14091        );
14092
14093        ctx.close().await;
14094    }
14095
14096    #[tokio::test(flavor = "multi_thread")]
14097    async fn test_new_sketch_twice_using_same_plane() {
14098        let initial_source = "\
14099sketch1 = sketch(on = XY) {
14100}
14101";
14102
14103        let program = Program::parse(initial_source).unwrap().0.unwrap();
14104
14105        let mut frontend = FrontendState::new();
14106        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14107        let version = Version(0);
14108
14109        frontend.hack_set_program(&ctx, program).await.unwrap();
14110
14111        let sketch_args = SketchCtor {
14112            on: Plane::Default(PlaneName::Xy),
14113        };
14114        let (src_delta, _, _) = frontend
14115            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14116            .await
14117            .unwrap();
14118
14119        assert_eq!(
14120            src_delta.text.as_str(),
14121            "\
14122sketch1 = sketch(on = XY) {
14123}
14124sketch001 = sketch(on = XY) {
14125}
14126"
14127        );
14128
14129        ctx.close().await;
14130    }
14131
14132    #[tokio::test(flavor = "multi_thread")]
14133    async fn test_sketch_mode_reuses_cached_on_expression() {
14134        let initial_source = "\
14135width = 2mm
14136sketch(on = offsetPlane(XY, offset = width)) {
14137  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14138  distance([line1.start, line1.end]) == width
14139}
14140";
14141        let program = Program::parse(initial_source).unwrap().0.unwrap();
14142
14143        let mut frontend = FrontendState::new();
14144        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14145        let mock_ctx = ExecutorContext::new_mock(None).await;
14146        let version = Version(0);
14147        let project_id = ProjectId(0);
14148        let file_id = FileId(0);
14149
14150        frontend.hack_set_program(&ctx, program).await.unwrap();
14151        let initial_object_count = frontend.scene_graph.objects.len();
14152        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14153            .expect("Expected sketch object to exist")
14154            .id;
14155
14156        // Entering sketch mode should reuse cached `on` expression state
14157        // (offsetPlane result), not fail or create extra on-surface objects.
14158        let scene_delta = frontend
14159            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14160            .await
14161            .unwrap();
14162        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14163
14164        // A follow-up sketch-mode execution should keep the same stable object
14165        // graph shape as well.
14166        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14167        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14168
14169        ctx.close().await;
14170        mock_ctx.close().await;
14171    }
14172
14173    #[tokio::test(flavor = "multi_thread")]
14174    async fn test_multiple_sketch_blocks() {
14175        let initial_source = "\
14176// Cube that requires the engine.
14177width = 2
14178sketch001 = startSketchOn(XY)
14179profile001 = startProfile(sketch001, at = [0, 0])
14180  |> yLine(length = width, tag = $seg1)
14181  |> xLine(length = width)
14182  |> yLine(length = -width)
14183  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14184  |> close()
14185extrude001 = extrude(profile001, length = width)
14186
14187// Get a value that requires the engine.
14188x = segLen(seg1)
14189
14190// Triangle with side length 2*x.
14191sketch(on = XY) {
14192  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14193  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14194  coincident([line1.end, line2.start])
14195  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14196  coincident([line2.end, line3.start])
14197  coincident([line3.end, line1.start])
14198  equalLength([line3, line1])
14199  equalLength([line1, line2])
14200  distance([line1.start, line1.end]) == 2*x
14201}
14202
14203// Line segment with length x.
14204sketch2 = sketch(on = XY) {
14205  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14206  distance([line1.start, line1.end]) == x
14207}
14208";
14209
14210        let program = Program::parse(initial_source).unwrap().0.unwrap();
14211
14212        let mut frontend = FrontendState::new();
14213
14214        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14215        let mock_ctx = ExecutorContext::new_mock(None).await;
14216        let version = Version(0);
14217        let project_id = ProjectId(0);
14218        let file_id = FileId(0);
14219
14220        frontend.hack_set_program(&ctx, program).await.unwrap();
14221        let sketch_objects = frontend
14222            .scene_graph
14223            .objects
14224            .iter()
14225            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14226            .collect::<Vec<_>>();
14227        let sketch1_id = sketch_objects.first().unwrap().id;
14228        let sketch2_id = sketch_objects.get(1).unwrap().id;
14229        // First point in sketch1.
14230        let point1_id = ObjectId(sketch1_id.0 + 1);
14231        // First point in sketch2.
14232        let point2_id = ObjectId(sketch2_id.0 + 1);
14233
14234        // Edit the first sketch. Objects before the sketch block should be
14235        // present from execution cache so that we can sketch on prior planes,
14236        // for example. Objects after the first sketch block should not be
14237        // present since those statements are skipped in sketch mode.
14238        //
14239        // - startSketchOn(XY) Plane 1
14240        // - sketch on=XY Plane 1
14241        // - Sketch block 16
14242        let scene_delta = frontend
14243            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14244            .await
14245            .unwrap();
14246        assert_eq!(
14247            scene_delta.new_graph.objects.len(),
14248            18,
14249            "{:#?}",
14250            scene_delta.new_graph.objects
14251        );
14252
14253        // Edit a point in the first sketch.
14254        let point_ctor = PointCtor {
14255            position: Point2d {
14256                x: Expr::Var(Number {
14257                    value: 1.0,
14258                    units: NumericSuffix::Mm,
14259                }),
14260                y: Expr::Var(Number {
14261                    value: 2.0,
14262                    units: NumericSuffix::Mm,
14263                }),
14264            },
14265        };
14266        let segments = vec![ExistingSegmentCtor {
14267            id: point1_id,
14268            ctor: SegmentCtor::Point(point_ctor),
14269        }];
14270        let (src_delta, _) = frontend
14271            .edit_segments(&mock_ctx, version, sketch1_id, segments)
14272            .await
14273            .unwrap();
14274        // Only the first sketch block changes.
14275        assert_eq!(
14276            src_delta.text.as_str(),
14277            "\
14278// Cube that requires the engine.
14279width = 2
14280sketch001 = startSketchOn(XY)
14281profile001 = startProfile(sketch001, at = [0, 0])
14282  |> yLine(length = width, tag = $seg1)
14283  |> xLine(length = width)
14284  |> yLine(length = -width)
14285  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14286  |> close()
14287extrude001 = extrude(profile001, length = width)
14288
14289// Get a value that requires the engine.
14290x = segLen(seg1)
14291
14292// Triangle with side length 2*x.
14293sketch(on = XY) {
14294  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14295  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14296  coincident([line1.end, line2.start])
14297  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14298  coincident([line2.end, line3.start])
14299  coincident([line3.end, line1.start])
14300  equalLength([line3, line1])
14301  equalLength([line1, line2])
14302  distance([line1.start, line1.end]) == 2 * x
14303}
14304
14305// Line segment with length x.
14306sketch2 = sketch(on = XY) {
14307  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14308  distance([line1.start, line1.end]) == x
14309}
14310"
14311        );
14312        let edited_sketch1_source = src_delta.text.clone();
14313
14314        // Execute mock to simulate drag end.
14315        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14316        assert_eq!(src_delta.text, edited_sketch1_source);
14317        // Exit sketch. Objects from the entire program should be present.
14318        //
14319        // - startSketchOn(XY) Plane 1
14320        // - sketch on=XY Plane 1
14321        // - Sketch block 16
14322        // - sketch on=XY cached
14323        // - Sketch block 5
14324        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14325        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14326
14327        // Edit the second sketch.
14328        //
14329        // - startSketchOn(XY) Plane 1
14330        // - sketch on=XY Plane 1
14331        // - Sketch block 16
14332        // - sketch on=XY cached
14333        // - Sketch block 5
14334        let scene_delta = frontend
14335            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14336            .await
14337            .unwrap();
14338        assert_eq!(
14339            scene_delta.new_graph.objects.len(),
14340            24,
14341            "{:#?}",
14342            scene_delta.new_graph.objects
14343        );
14344
14345        // Edit a point in the second sketch.
14346        let point_ctor = PointCtor {
14347            position: Point2d {
14348                x: Expr::Var(Number {
14349                    value: 3.0,
14350                    units: NumericSuffix::Mm,
14351                }),
14352                y: Expr::Var(Number {
14353                    value: 4.0,
14354                    units: NumericSuffix::Mm,
14355                }),
14356            },
14357        };
14358        let segments = vec![ExistingSegmentCtor {
14359            id: point2_id,
14360            ctor: SegmentCtor::Point(point_ctor),
14361        }];
14362        let (src_delta, _) = frontend
14363            .edit_segments(&mock_ctx, version, sketch2_id, segments)
14364            .await
14365            .unwrap();
14366        // Only the second sketch block changes.
14367        assert_eq!(
14368            src_delta.text.as_str(),
14369            "\
14370// Cube that requires the engine.
14371width = 2
14372sketch001 = startSketchOn(XY)
14373profile001 = startProfile(sketch001, at = [0, 0])
14374  |> yLine(length = width, tag = $seg1)
14375  |> xLine(length = width)
14376  |> yLine(length = -width)
14377  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14378  |> close()
14379extrude001 = extrude(profile001, length = width)
14380
14381// Get a value that requires the engine.
14382x = segLen(seg1)
14383
14384// Triangle with side length 2*x.
14385sketch(on = XY) {
14386  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14387  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14388  coincident([line1.end, line2.start])
14389  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14390  coincident([line2.end, line3.start])
14391  coincident([line3.end, line1.start])
14392  equalLength([line3, line1])
14393  equalLength([line1, line2])
14394  distance([line1.start, line1.end]) == 2 * x
14395}
14396
14397// Line segment with length x.
14398sketch2 = sketch(on = XY) {
14399  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14400  distance([line1.start, line1.end]) == x
14401}
14402"
14403        );
14404        let edited_sketch2_source = src_delta.text.clone();
14405
14406        // Execute mock to simulate drag end.
14407        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14408        assert_eq!(src_delta.text, edited_sketch2_source);
14409
14410        ctx.close().await;
14411        mock_ctx.close().await;
14412    }
14413
14414    #[tokio::test(flavor = "multi_thread")]
14415    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14416        clear_mem_cache().await;
14417
14418        let source = r#"sketch001 = sketch(on = XZ) {
14419  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14420}
14421sketch002 = sketch(on = XY) {
14422  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14423  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14424  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14425  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14426  coincident([line1.end, line2.start])
14427  coincident([line2.end, line3.start])
14428  coincident([line3.end, line4.start])
14429  coincident([line4.end, line1.start])
14430  parallel([line2, line4])
14431  parallel([line3, line1])
14432  perpendicular([line1, line2])
14433  horizontal(line3)
14434  coincident([line1.start, ORIGIN])
14435}
14436"#;
14437
14438        let program = Program::parse(source).unwrap().0.unwrap();
14439        let mut frontend = FrontendState::new();
14440        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14441        let mock_ctx = ExecutorContext::new_mock(None).await;
14442        let version = Version(0);
14443        let project_id = ProjectId(0);
14444        let file_id = FileId(0);
14445
14446        frontend.hack_set_program(&ctx, program).await.unwrap();
14447        let sketch_objects = frontend
14448            .scene_graph
14449            .objects
14450            .iter()
14451            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14452            .collect::<Vec<_>>();
14453        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14454
14455        let sketch1_id = sketch_objects[0].id;
14456        let sketch2_id = sketch_objects[1].id;
14457
14458        frontend
14459            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14460            .await
14461            .unwrap();
14462        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14463
14464        let scene_delta = frontend
14465            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14466            .await
14467            .unwrap();
14468        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14469
14470        clear_mem_cache().await;
14471        ctx.close().await;
14472        mock_ctx.close().await;
14473    }
14474
14475    // Regression tests: operations on source code with extra whitespace/newlines.
14476    // These test that NodePath-based lookups work correctly when source ranges
14477    // are shifted by extra whitespace that wouldn't be present after formatting.
14478
14479    #[tokio::test(flavor = "multi_thread")]
14480    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14481        // Extra newlines after @settings line - this shifts all source ranges.
14482        let initial_source = "@settings(defaultLengthUnit = mm)
14483
14484
14485
14486sketch001 = sketch(on = XY) {
14487  point(at = [1in, 2in])
14488}
14489";
14490
14491        let program = Program::parse(initial_source).unwrap().0.unwrap();
14492        let mut frontend = FrontendState::new();
14493
14494        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14495        let mock_ctx = ExecutorContext::new_mock(None).await;
14496        let version = Version(0);
14497        let project_id = ProjectId(0);
14498        let file_id = FileId(0);
14499
14500        frontend.hack_set_program(&ctx, program).await.unwrap();
14501        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14502        let sketch_id = sketch_object.id;
14503
14504        // Edit sketch should succeed despite extra newlines.
14505        frontend
14506            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14507            .await
14508            .unwrap();
14509
14510        // Add a new point to the sketch.
14511        let point_ctor = PointCtor {
14512            position: Point2d {
14513                x: Expr::Number(Number {
14514                    value: 5.0,
14515                    units: NumericSuffix::Mm,
14516                }),
14517                y: Expr::Number(Number {
14518                    value: 6.0,
14519                    units: NumericSuffix::Mm,
14520                }),
14521            },
14522        };
14523        let segment = SegmentCtor::Point(point_ctor);
14524        let (src_delta, scene_delta) = frontend
14525            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14526            .await
14527            .unwrap();
14528        // After adding a point, the source should be reformatted with standard whitespace.
14529        assert!(
14530            src_delta.text.contains("point(at = [5mm, 6mm])"),
14531            "Expected new point in source, got: {}",
14532            src_delta.text
14533        );
14534        assert!(!scene_delta.new_objects.is_empty());
14535
14536        ctx.close().await;
14537        mock_ctx.close().await;
14538    }
14539
14540    #[tokio::test(flavor = "multi_thread")]
14541    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14542        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14543
14544        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14545        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14546
14547        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14548        assert!(
14549            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14550            "Expected experimental settings to be added to source"
14551        );
14552    }
14553
14554    #[tokio::test(flavor = "multi_thread")]
14555    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14556        // Extra newlines after @settings, with an empty sketch block.
14557        let initial_source = "@settings(defaultLengthUnit = mm)
14558
14559
14560
14561s = sketch(on = XY) {}
14562";
14563
14564        let program = Program::parse(initial_source).unwrap().0.unwrap();
14565        let mut frontend = FrontendState::new();
14566
14567        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14568        let mock_ctx = ExecutorContext::new_mock(None).await;
14569        let version = Version(0);
14570
14571        frontend.hack_set_program(&ctx, program).await.unwrap();
14572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14573        let sketch_id = sketch_object.id;
14574
14575        let line_ctor = LineCtor {
14576            start: Point2d {
14577                x: Expr::Number(Number {
14578                    value: 0.0,
14579                    units: NumericSuffix::Mm,
14580                }),
14581                y: Expr::Number(Number {
14582                    value: 0.0,
14583                    units: NumericSuffix::Mm,
14584                }),
14585            },
14586            end: Point2d {
14587                x: Expr::Number(Number {
14588                    value: 10.0,
14589                    units: NumericSuffix::Mm,
14590                }),
14591                y: Expr::Number(Number {
14592                    value: 10.0,
14593                    units: NumericSuffix::Mm,
14594                }),
14595            },
14596            construction: None,
14597        };
14598        let segment = SegmentCtor::Line(line_ctor);
14599        let (src_delta, scene_delta) = frontend
14600            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14601            .await
14602            .unwrap();
14603        assert!(
14604            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14605            "Expected line in source, got: {}",
14606            src_delta.text
14607        );
14608        // Line creates start point, end point, and line segment.
14609        assert_eq!(scene_delta.new_objects.len(), 3);
14610
14611        ctx.close().await;
14612        mock_ctx.close().await;
14613    }
14614
14615    #[tokio::test(flavor = "multi_thread")]
14616    async fn test_extra_newlines_between_operations_edit_line() {
14617        // Extra newlines between @settings and sketch, and inside the sketch block.
14618        let initial_source = "@settings(defaultLengthUnit = mm)
14619
14620
14621sketch001 = sketch(on = XY) {
14622
14623  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14624
14625}
14626";
14627
14628        let program = Program::parse(initial_source).unwrap().0.unwrap();
14629        let mut frontend = FrontendState::new();
14630
14631        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14632        let mock_ctx = ExecutorContext::new_mock(None).await;
14633        let version = Version(0);
14634        let project_id = ProjectId(0);
14635        let file_id = FileId(0);
14636
14637        frontend.hack_set_program(&ctx, program).await.unwrap();
14638        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14639        let sketch_id = sketch_object.id;
14640        let sketch = expect_sketch(sketch_object);
14641
14642        // Extract segment IDs before edit_sketch borrows frontend mutably.
14643        let line_id = sketch
14644            .segments
14645            .iter()
14646            .copied()
14647            .find(|seg_id| {
14648                matches!(
14649                    &frontend.scene_graph.objects[seg_id.0].kind,
14650                    ObjectKind::Segment {
14651                        segment: Segment::Line(_)
14652                    }
14653                )
14654            })
14655            .expect("Expected a line segment in sketch");
14656
14657        // Enter sketch edit mode.
14658        frontend
14659            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14660            .await
14661            .unwrap();
14662
14663        // Edit the line.
14664        let line_ctor = LineCtor {
14665            start: Point2d {
14666                x: Expr::Var(Number {
14667                    value: 1.0,
14668                    units: NumericSuffix::Mm,
14669                }),
14670                y: Expr::Var(Number {
14671                    value: 2.0,
14672                    units: NumericSuffix::Mm,
14673                }),
14674            },
14675            end: Point2d {
14676                x: Expr::Var(Number {
14677                    value: 13.0,
14678                    units: NumericSuffix::Mm,
14679                }),
14680                y: Expr::Var(Number {
14681                    value: 14.0,
14682                    units: NumericSuffix::Mm,
14683                }),
14684            },
14685            construction: None,
14686        };
14687        let segments = vec![ExistingSegmentCtor {
14688            id: line_id,
14689            ctor: SegmentCtor::Line(line_ctor),
14690        }];
14691        let (src_delta, _scene_delta) = frontend
14692            .edit_segments(&mock_ctx, version, sketch_id, segments)
14693            .await
14694            .unwrap();
14695        assert!(
14696            src_delta
14697                .text
14698                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14699            "Expected edited line in source, got: {}",
14700            src_delta.text
14701        );
14702
14703        ctx.close().await;
14704        mock_ctx.close().await;
14705    }
14706
14707    #[tokio::test(flavor = "multi_thread")]
14708    async fn test_extra_newlines_delete_segment() {
14709        // Extra whitespace before and after the sketch block.
14710        let initial_source = "@settings(defaultLengthUnit = mm)
14711
14712
14713
14714sketch001 = sketch(on = XY) {
14715  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14716}
14717";
14718
14719        let program = Program::parse(initial_source).unwrap().0.unwrap();
14720        let mut frontend = FrontendState::new();
14721
14722        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14723        let mock_ctx = ExecutorContext::new_mock(None).await;
14724        let version = Version(0);
14725
14726        frontend.hack_set_program(&ctx, program).await.unwrap();
14727        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14728        let sketch_id = sketch_object.id;
14729        let sketch = expect_sketch(sketch_object);
14730
14731        // The sketch should have 3 segments: start point, center point, and the circle.
14732        assert_eq!(sketch.segments.len(), 3);
14733        let circle_id = sketch.segments[2];
14734
14735        // Delete the circle despite extra newlines in original source.
14736        let (src_delta, scene_delta) = frontend
14737            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14738            .await
14739            .unwrap();
14740        assert!(
14741            src_delta.text.contains("sketch(on = XY) {"),
14742            "Expected sketch block in source, got: {}",
14743            src_delta.text
14744        );
14745        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14746        let new_sketch = expect_sketch(new_sketch_object);
14747        assert_eq!(new_sketch.segments.len(), 0);
14748
14749        ctx.close().await;
14750        mock_ctx.close().await;
14751    }
14752
14753    #[tokio::test(flavor = "multi_thread")]
14754    async fn test_unformatted_source_add_arc() {
14755        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
14756        let initial_source = "@settings(defaultLengthUnit = mm)
14757
14758
14759
14760
14761sketch001 = sketch(on = XY) {
14762}
14763";
14764
14765        let program = Program::parse(initial_source).unwrap().0.unwrap();
14766        let mut frontend = FrontendState::new();
14767
14768        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14769        let mock_ctx = ExecutorContext::new_mock(None).await;
14770        let version = Version(0);
14771
14772        frontend.hack_set_program(&ctx, program).await.unwrap();
14773        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14774        let sketch_id = sketch_object.id;
14775
14776        let arc_ctor = ArcCtor {
14777            start: Point2d {
14778                x: Expr::Var(Number {
14779                    value: 5.0,
14780                    units: NumericSuffix::Mm,
14781                }),
14782                y: Expr::Var(Number {
14783                    value: 0.0,
14784                    units: NumericSuffix::Mm,
14785                }),
14786            },
14787            end: Point2d {
14788                x: Expr::Var(Number {
14789                    value: 0.0,
14790                    units: NumericSuffix::Mm,
14791                }),
14792                y: Expr::Var(Number {
14793                    value: 5.0,
14794                    units: NumericSuffix::Mm,
14795                }),
14796            },
14797            center: Point2d {
14798                x: Expr::Var(Number {
14799                    value: 0.0,
14800                    units: NumericSuffix::Mm,
14801                }),
14802                y: Expr::Var(Number {
14803                    value: 0.0,
14804                    units: NumericSuffix::Mm,
14805                }),
14806            },
14807            construction: None,
14808        };
14809        let segment = SegmentCtor::Arc(arc_ctor);
14810        let (src_delta, scene_delta) = frontend
14811            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14812            .await
14813            .unwrap();
14814        assert!(
14815            src_delta
14816                .text
14817                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14818            "Expected arc in source, got: {}",
14819            src_delta.text
14820        );
14821        assert!(!scene_delta.new_objects.is_empty());
14822
14823        ctx.close().await;
14824        mock_ctx.close().await;
14825    }
14826
14827    #[tokio::test(flavor = "multi_thread")]
14828    async fn test_extra_newlines_add_circle() {
14829        // Extra blank lines between settings and sketch.
14830        let initial_source = "@settings(defaultLengthUnit = mm)
14831
14832
14833
14834sketch001 = sketch(on = XY) {
14835}
14836";
14837
14838        let program = Program::parse(initial_source).unwrap().0.unwrap();
14839        let mut frontend = FrontendState::new();
14840
14841        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14842        let mock_ctx = ExecutorContext::new_mock(None).await;
14843        let version = Version(0);
14844
14845        frontend.hack_set_program(&ctx, program).await.unwrap();
14846        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14847        let sketch_id = sketch_object.id;
14848
14849        let circle_ctor = CircleCtor {
14850            start: Point2d {
14851                x: Expr::Var(Number {
14852                    value: 5.0,
14853                    units: NumericSuffix::Mm,
14854                }),
14855                y: Expr::Var(Number {
14856                    value: 0.0,
14857                    units: NumericSuffix::Mm,
14858                }),
14859            },
14860            center: Point2d {
14861                x: Expr::Var(Number {
14862                    value: 0.0,
14863                    units: NumericSuffix::Mm,
14864                }),
14865                y: Expr::Var(Number {
14866                    value: 0.0,
14867                    units: NumericSuffix::Mm,
14868                }),
14869            },
14870            construction: None,
14871        };
14872        let segment = SegmentCtor::Circle(circle_ctor);
14873        let (src_delta, scene_delta) = frontend
14874            .add_segment(&mock_ctx, version, sketch_id, segment, None)
14875            .await
14876            .unwrap();
14877        assert!(
14878            src_delta
14879                .text
14880                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14881            "Expected circle in source, got: {}",
14882            src_delta.text
14883        );
14884        assert!(!scene_delta.new_objects.is_empty());
14885
14886        ctx.close().await;
14887        mock_ctx.close().await;
14888    }
14889
14890    #[tokio::test(flavor = "multi_thread")]
14891    async fn test_extra_newlines_add_constraint() {
14892        // Extra newlines with a sketch containing two lines - add a coincident constraint.
14893        let initial_source = "@settings(defaultLengthUnit = mm)
14894
14895
14896
14897sketch001 = sketch(on = XY) {
14898  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14899  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14900}
14901";
14902
14903        let program = Program::parse(initial_source).unwrap().0.unwrap();
14904        let mut frontend = FrontendState::new();
14905
14906        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14907        let mock_ctx = ExecutorContext::new_mock(None).await;
14908        let version = Version(0);
14909        let project_id = ProjectId(0);
14910        let file_id = FileId(0);
14911
14912        frontend.hack_set_program(&ctx, program).await.unwrap();
14913        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14914        let sketch_id = sketch_object.id;
14915        let sketch = expect_sketch(sketch_object);
14916
14917        // Extract segment data before edit_sketch borrows frontend mutably.
14918        let line_ids: Vec<ObjectId> = sketch
14919            .segments
14920            .iter()
14921            .copied()
14922            .filter(|seg_id| {
14923                matches!(
14924                    &frontend.scene_graph.objects[seg_id.0].kind,
14925                    ObjectKind::Segment {
14926                        segment: Segment::Line(_)
14927                    }
14928                )
14929            })
14930            .collect();
14931        assert_eq!(line_ids.len(), 2, "Expected two line segments");
14932
14933        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14934        let ObjectKind::Segment {
14935            segment: Segment::Line(line1_data),
14936        } = &line1.kind
14937        else {
14938            panic!("Expected line");
14939        };
14940        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14941        let ObjectKind::Segment {
14942            segment: Segment::Line(line2_data),
14943        } = &line2.kind
14944        else {
14945            panic!("Expected line");
14946        };
14947
14948        // Build constraint before entering sketch mode.
14949        let constraint = Constraint::Coincident(Coincident {
14950            segments: vec![line1_data.end.into(), line2_data.start.into()],
14951        });
14952
14953        // Enter sketch edit mode.
14954        frontend
14955            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14956            .await
14957            .unwrap();
14958        let (src_delta, _scene_delta) = frontend
14959            .add_constraint(&mock_ctx, version, sketch_id, constraint)
14960            .await
14961            .unwrap();
14962        assert!(
14963            src_delta.text.contains("coincident("),
14964            "Expected coincident constraint in source, got: {}",
14965            src_delta.text
14966        );
14967
14968        ctx.close().await;
14969        mock_ctx.close().await;
14970    }
14971
14972    #[tokio::test(flavor = "multi_thread")]
14973    async fn test_extra_newlines_add_line_then_edit_line() {
14974        // Extra newlines after @settings - add a line, then edit it.
14975        let initial_source = "@settings(defaultLengthUnit = mm)
14976
14977
14978
14979sketch001 = sketch(on = XY) {
14980}
14981";
14982
14983        let program = Program::parse(initial_source).unwrap().0.unwrap();
14984        let mut frontend = FrontendState::new();
14985
14986        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14987        let mock_ctx = ExecutorContext::new_mock(None).await;
14988        let version = Version(0);
14989
14990        frontend.hack_set_program(&ctx, program).await.unwrap();
14991        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14992        let sketch_id = sketch_object.id;
14993
14994        // Add a line.
14995        let line_ctor = LineCtor {
14996            start: Point2d {
14997                x: Expr::Number(Number {
14998                    value: 0.0,
14999                    units: NumericSuffix::Mm,
15000                }),
15001                y: Expr::Number(Number {
15002                    value: 0.0,
15003                    units: NumericSuffix::Mm,
15004                }),
15005            },
15006            end: Point2d {
15007                x: Expr::Number(Number {
15008                    value: 10.0,
15009                    units: NumericSuffix::Mm,
15010                }),
15011                y: Expr::Number(Number {
15012                    value: 10.0,
15013                    units: NumericSuffix::Mm,
15014                }),
15015            },
15016            construction: None,
15017        };
15018        let segment = SegmentCtor::Line(line_ctor);
15019        let (src_delta, scene_delta) = frontend
15020            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15021            .await
15022            .unwrap();
15023        assert!(
15024            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15025            "Expected line in source after add, got: {}",
15026            src_delta.text
15027        );
15028        // Line creates start point, end point, and line segment.
15029        let line_id = *scene_delta.new_objects.last().unwrap();
15030
15031        // Edit the line.
15032        let line_ctor = LineCtor {
15033            start: Point2d {
15034                x: Expr::Number(Number {
15035                    value: 1.0,
15036                    units: NumericSuffix::Mm,
15037                }),
15038                y: Expr::Number(Number {
15039                    value: 2.0,
15040                    units: NumericSuffix::Mm,
15041                }),
15042            },
15043            end: Point2d {
15044                x: Expr::Number(Number {
15045                    value: 13.0,
15046                    units: NumericSuffix::Mm,
15047                }),
15048                y: Expr::Number(Number {
15049                    value: 14.0,
15050                    units: NumericSuffix::Mm,
15051                }),
15052            },
15053            construction: None,
15054        };
15055        let segments = vec![ExistingSegmentCtor {
15056            id: line_id,
15057            ctor: SegmentCtor::Line(line_ctor),
15058        }];
15059        let (src_delta, scene_delta) = frontend
15060            .edit_segments(&mock_ctx, version, sketch_id, segments)
15061            .await
15062            .unwrap();
15063        assert!(
15064            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15065            "Expected edited line in source, got: {}",
15066            src_delta.text
15067        );
15068        assert_eq!(scene_delta.new_objects, vec![]);
15069
15070        ctx.close().await;
15071        mock_ctx.close().await;
15072    }
15073}