Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::collections::AhashIndexSet;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::Artifact;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23#[cfg(feature = "artifact-graph")]
24use crate::execution::CapSubType;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::cache::SketchModeState;
28use crate::execution::cache::clear_mem_cache;
29use crate::execution::cache::read_old_memory;
30use crate::execution::cache::write_old_memory;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::Distance;
36use crate::front::EqualRadius;
37use crate::front::Error;
38use crate::front::ExecResult;
39use crate::front::FixedPoint;
40use crate::front::Freedom;
41use crate::front::LinesEqualLength;
42use crate::front::Midpoint;
43use crate::front::Object;
44use crate::front::Parallel;
45use crate::front::Perpendicular;
46use crate::front::PointCtor;
47use crate::front::Symmetric;
48use crate::front::Tangent;
49use crate::frontend::api::Expr;
50use crate::frontend::api::FileId;
51use crate::frontend::api::Number;
52use crate::frontend::api::ObjectId;
53use crate::frontend::api::ObjectKind;
54use crate::frontend::api::Plane;
55use crate::frontend::api::ProjectId;
56use crate::frontend::api::RestoreSketchCheckpointOutcome;
57use crate::frontend::api::SceneGraph;
58use crate::frontend::api::SceneGraphDelta;
59use crate::frontend::api::SketchCheckpointId;
60use crate::frontend::api::SourceDelta;
61use crate::frontend::api::SourceRef;
62use crate::frontend::api::Version;
63use crate::frontend::modify::find_defined_names;
64use crate::frontend::modify::next_free_name;
65use crate::frontend::modify::next_free_name_with_padding;
66use crate::frontend::sketch::Coincident;
67use crate::frontend::sketch::Constraint;
68use crate::frontend::sketch::ConstraintSegment;
69use crate::frontend::sketch::Diameter;
70use crate::frontend::sketch::ExistingSegmentCtor;
71use crate::frontend::sketch::Horizontal;
72use crate::frontend::sketch::LineCtor;
73use crate::frontend::sketch::Point2d;
74use crate::frontend::sketch::Radius;
75use crate::frontend::sketch::Segment;
76use crate::frontend::sketch::SegmentCtor;
77use crate::frontend::sketch::SketchApi;
78use crate::frontend::sketch::SketchCtor;
79use crate::frontend::sketch::Vertical;
80use crate::frontend::traverse::MutateBodyItem;
81use crate::frontend::traverse::TraversalReturn;
82use crate::frontend::traverse::Visitor;
83use crate::frontend::traverse::dfs_mut;
84use crate::id::IncIdGenerator;
85use crate::parsing::ast::types as ast;
86use crate::pretty::NumericSuffix;
87use crate::std::constraints::LinesAtAngleKind;
88use crate::walk::NodeMut;
89use crate::walk::Visitable;
90
91pub(crate) mod api;
92pub(crate) mod modify;
93pub(crate) mod sketch;
94
95pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
96
97#[derive(Debug, Clone)]
98struct SketchCheckpoint {
99    id: SketchCheckpointId,
100    source: SourceDelta,
101    program: Program,
102    scene_graph: SceneGraph,
103    exec_outcome: ExecOutcome,
104    point_freedom_cache: HashMap<ObjectId, Freedom>,
105    mock_memory: Option<SketchModeState>,
106}
107mod traverse;
108pub(crate) mod trim;
109
110struct ArcSizeConstraintParams {
111    points: Vec<ObjectId>,
112    function_name: &'static str,
113    value: f64,
114    units: NumericSuffix,
115    label_position: Option<Point2d<Number>>,
116    constraint_type_name: &'static str,
117}
118
119const POINT_FN: &str = "point";
120const POINT_AT_PARAM: &str = "at";
121const LINE_FN: &str = "line";
122const LINE_VARIABLE: &str = "line";
123const LINE_START_PARAM: &str = "start";
124const LINE_END_PARAM: &str = "end";
125const ARC_FN: &str = "arc";
126const ARC_VARIABLE: &str = "arc";
127const ARC_START_PARAM: &str = "start";
128const ARC_END_PARAM: &str = "end";
129const ARC_CENTER_PARAM: &str = "center";
130const CIRCLE_FN: &str = "circle";
131const CIRCLE_VARIABLE: &str = "circle";
132const CIRCLE_START_PARAM: &str = "start";
133const CIRCLE_CENTER_PARAM: &str = "center";
134const LABEL_POSITION_PARAM: &str = "labelPosition";
135
136const COINCIDENT_FN: &str = "coincident";
137const DIAMETER_FN: &str = "diameter";
138const DISTANCE_FN: &str = "distance";
139const FIXED_FN: &str = "fixed";
140const ANGLE_FN: &str = "angle";
141const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
142const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
143const EQUAL_LENGTH_FN: &str = "equalLength";
144const EQUAL_RADIUS_FN: &str = "equalRadius";
145const HORIZONTAL_FN: &str = "horizontal";
146const MIDPOINT_FN: &str = "midpoint";
147const MIDPOINT_POINT_PARAM: &str = "point";
148const RADIUS_FN: &str = "radius";
149const SYMMETRIC_FN: &str = "symmetric";
150const SYMMETRIC_AXIS_PARAM: &str = "axis";
151const TANGENT_FN: &str = "tangent";
152const VERTICAL_FN: &str = "vertical";
153
154const LINE_PROPERTY_START: &str = "start";
155const LINE_PROPERTY_END: &str = "end";
156
157const ARC_PROPERTY_START: &str = "start";
158const ARC_PROPERTY_END: &str = "end";
159const ARC_PROPERTY_CENTER: &str = "center";
160const CIRCLE_PROPERTY_START: &str = "start";
161const CIRCLE_PROPERTY_CENTER: &str = "center";
162
163const CONSTRUCTION_PARAM: &str = "construction";
164
165#[derive(Debug, Clone, Copy)]
166enum EditDeleteKind {
167    Edit,
168    DeleteNonSketch,
169}
170
171impl EditDeleteKind {
172    /// Returns true if this edit is any type of deletion.
173    fn is_delete(&self) -> bool {
174        match self {
175            EditDeleteKind::Edit => false,
176            EditDeleteKind::DeleteNonSketch => true,
177        }
178    }
179
180    fn to_change_kind(self) -> ChangeKind {
181        match self {
182            EditDeleteKind::Edit => ChangeKind::Edit,
183            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
184        }
185    }
186}
187
188#[derive(Debug, Clone, Copy)]
189enum ChangeKind {
190    Add,
191    Edit,
192    Delete,
193    None,
194}
195
196#[derive(Debug, Clone, Serialize, ts_rs::TS)]
197#[ts(export, export_to = "FrontendApi.ts")]
198#[serde(tag = "type")]
199pub enum SetProgramOutcome {
200    #[serde(rename_all = "camelCase")]
201    Success {
202        scene_graph: Box<SceneGraph>,
203        exec_outcome: Box<ExecOutcome>,
204        checkpoint_id: Option<SketchCheckpointId>,
205    },
206    #[serde(rename_all = "camelCase")]
207    ExecFailure { error: Box<KclErrorWithOutputs> },
208}
209
210#[derive(Debug, Clone)]
211pub struct FrontendState {
212    program: Program,
213    scene_graph: SceneGraph,
214    /// Stores the last known freedom value for each point object.
215    /// This allows us to preserve freedom values when freedom analysis isn't run.
216    point_freedom_cache: HashMap<ObjectId, Freedom>,
217    sketch_checkpoints: VecDeque<SketchCheckpoint>,
218    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
219}
220
221impl Default for FrontendState {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227impl FrontendState {
228    pub fn new() -> Self {
229        Self {
230            program: Program::empty(),
231            scene_graph: SceneGraph {
232                project: ProjectId(0),
233                file: FileId(0),
234                version: Version(0),
235                objects: Default::default(),
236                settings: Default::default(),
237                sketch_mode: Default::default(),
238            },
239            point_freedom_cache: HashMap::new(),
240            sketch_checkpoints: VecDeque::new(),
241            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
242        }
243    }
244
245    /// Get a reference to the scene graph
246    pub fn scene_graph(&self) -> &SceneGraph {
247        &self.scene_graph
248    }
249
250    pub fn default_length_unit(&self) -> UnitLength {
251        self.program
252            .meta_settings()
253            .ok()
254            .flatten()
255            .map(|settings| settings.default_length_units)
256            .unwrap_or(UnitLength::Millimeters)
257    }
258
259    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
260        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
261
262        let checkpoint = SketchCheckpoint {
263            id: checkpoint_id,
264            source: SourceDelta {
265                text: source_from_ast(&self.program.ast),
266            },
267            program: self.program.clone(),
268            scene_graph: self.scene_graph.clone(),
269            exec_outcome,
270            point_freedom_cache: self.point_freedom_cache.clone(),
271            mock_memory: read_old_memory().await,
272        };
273
274        self.sketch_checkpoints.push_back(checkpoint);
275        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
276            self.sketch_checkpoints.pop_front();
277        }
278
279        Ok(checkpoint_id)
280    }
281
282    pub async fn restore_sketch_checkpoint(
283        &mut self,
284        checkpoint_id: SketchCheckpointId,
285    ) -> api::Result<RestoreSketchCheckpointOutcome> {
286        let checkpoint = self
287            .sketch_checkpoints
288            .iter()
289            .find(|checkpoint| checkpoint.id == checkpoint_id)
290            .cloned()
291            .ok_or_else(|| Error {
292                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
293            })?;
294
295        self.program = checkpoint.program;
296        self.scene_graph = checkpoint.scene_graph.clone();
297        self.point_freedom_cache = checkpoint.point_freedom_cache;
298
299        if let Some(mock_memory) = checkpoint.mock_memory {
300            write_old_memory(mock_memory).await;
301        } else {
302            clear_mem_cache().await;
303        }
304
305        Ok(RestoreSketchCheckpointOutcome {
306            source_delta: checkpoint.source,
307            scene_graph_delta: SceneGraphDelta {
308                new_graph: checkpoint.scene_graph,
309                new_objects: Vec::new(),
310                invalidates_ids: true,
311                exec_outcome: checkpoint.exec_outcome,
312            },
313        })
314    }
315
316    pub fn clear_sketch_checkpoints(&mut self) {
317        self.sketch_checkpoints.clear();
318    }
319}
320
321impl SketchApi for FrontendState {
322    async fn execute_mock(
323        &mut self,
324        ctx: &ExecutorContext,
325        _version: Version,
326        sketch: ObjectId,
327    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
328        let sketch_block_ref =
329            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
330
331        let mut truncated_program = self.program.clone();
332        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
333            .map_err(KclErrorWithOutputs::no_outputs)?;
334
335        // Execute.
336        let outcome = ctx
337            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
338            .await?;
339        let new_source = source_from_ast(&self.program.ast);
340        let src_delta = SourceDelta { text: new_source };
341        // MockConfig::default() has freedom_analysis: true
342        let outcome = self.update_state_after_exec(outcome, true);
343        let scene_graph_delta = SceneGraphDelta {
344            new_graph: self.scene_graph.clone(),
345            new_objects: Default::default(),
346            invalidates_ids: false,
347            exec_outcome: outcome,
348        };
349        Ok((src_delta, scene_graph_delta))
350    }
351
352    async fn new_sketch(
353        &mut self,
354        ctx: &ExecutorContext,
355        _project: ProjectId,
356        _file: FileId,
357        _version: Version,
358        args: SketchCtor,
359    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
360        // TODO: Check version.
361
362        let mut new_ast = self.program.ast.clone();
363        // Create updated KCL source from args.
364        let mut plane_ast =
365            sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
366        let mut defined_names = find_defined_names(&new_ast);
367        let is_face_of_expr = matches!(
368            &plane_ast,
369            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
370        );
371        if is_face_of_expr {
372            let face_name = next_free_name_with_padding("face", &defined_names)
373                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
374            let face_decl = ast::VariableDeclaration::new(
375                ast::VariableDeclarator::new(&face_name, plane_ast),
376                ast::ItemVisibility::Default,
377                ast::VariableKind::Const,
378            );
379            new_ast
380                .body
381                .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
382                    face_decl,
383                ))));
384            defined_names.insert(face_name.clone());
385            plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
386        }
387        let sketch_ast = ast::SketchBlock {
388            arguments: vec![ast::LabeledArg {
389                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
390                arg: plane_ast,
391            }],
392            body: Default::default(),
393            is_being_edited: false,
394            non_code_meta: Default::default(),
395            digest: None,
396        };
397        // Add a sketch block as a variable declaration directly, avoiding
398        // source-range mutation on a no-src node.
399        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
400            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
401        let sketch_decl = ast::VariableDeclaration::new(
402            ast::VariableDeclarator::new(
403                &sketch_name,
404                ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
405            ),
406            ast::ItemVisibility::Default,
407            ast::VariableKind::Const,
408        );
409        new_ast
410            .body
411            .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
412                sketch_decl,
413            ))));
414        // Convert to string source to create real source ranges.
415        let new_source = source_from_ast(&new_ast);
416        // Parse the new source.
417        let (new_program, errors) = Program::parse(&new_source)
418            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
419        if !errors.is_empty() {
420            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
421                "Error parsing KCL source after adding sketch: {errors:?}"
422            ))));
423        }
424        let Some(new_program) = new_program else {
425            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
426                "No AST produced after adding sketch".to_owned(),
427            )));
428        };
429
430        // Make sure to only set this if there are no errors.
431        self.program = new_program.clone();
432
433        // We need to do an engine execute so that the plane object gets created
434        // and is cached.
435        let outcome = ctx.run_with_caching(new_program.clone()).await?;
436        let freedom_analysis_ran = true;
437
438        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
439
440        let Some(sketch_id) = self
441            .scene_graph
442            .objects
443            .iter()
444            .filter_map(|object| match object.kind {
445                ObjectKind::Sketch(_) => Some(object.id),
446                _ => None,
447            })
448            .max_by_key(|id| id.0)
449        else {
450            return Err(KclErrorWithOutputs::from_error_outcome(
451                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
452                outcome,
453            ));
454        };
455        // Store the object in the scene.
456        self.scene_graph.sketch_mode = Some(sketch_id);
457
458        let src_delta = SourceDelta { text: new_source };
459        let scene_graph_delta = SceneGraphDelta {
460            new_graph: self.scene_graph.clone(),
461            invalidates_ids: false,
462            new_objects: vec![sketch_id],
463            exec_outcome: outcome,
464        };
465        Ok((src_delta, scene_graph_delta, sketch_id))
466    }
467
468    async fn edit_sketch(
469        &mut self,
470        ctx: &ExecutorContext,
471        _project: ProjectId,
472        _file: FileId,
473        _version: Version,
474        sketch: ObjectId,
475    ) -> ExecResult<SceneGraphDelta> {
476        // TODO: Check version.
477
478        // Look up existing sketch.
479        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
480            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
481        })?;
482        let ObjectKind::Sketch(_) = &sketch_object.kind else {
483            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
484                "Object is not a sketch, it is {}",
485                sketch_object.kind.human_friendly_kind_with_article()
486            ))));
487        };
488        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
489
490        // Enter sketch mode by setting the sketch_mode.
491        self.scene_graph.sketch_mode = Some(sketch);
492
493        // Truncate after the sketch block for mock execution.
494        let mut truncated_program = self.program.clone();
495        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
496            .map_err(KclErrorWithOutputs::no_outputs)?;
497
498        // Execute in mock mode to ensure state is up to date. The caller will
499        // want freedom analysis to display segments correctly.
500        let outcome = ctx
501            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
502            .await?;
503
504        // MockConfig::default() has freedom_analysis: true
505        let outcome = self.update_state_after_exec(outcome, true);
506        let scene_graph_delta = SceneGraphDelta {
507            new_graph: self.scene_graph.clone(),
508            invalidates_ids: false,
509            new_objects: Vec::new(),
510            exec_outcome: outcome,
511        };
512        Ok(scene_graph_delta)
513    }
514
515    async fn exit_sketch(
516        &mut self,
517        ctx: &ExecutorContext,
518        _version: Version,
519        sketch: ObjectId,
520    ) -> ExecResult<SceneGraph> {
521        // TODO: Check version.
522        #[cfg(not(target_arch = "wasm32"))]
523        let _ = sketch;
524        #[cfg(target_arch = "wasm32")]
525        if self.scene_graph.sketch_mode != Some(sketch) {
526            web_sys::console::warn_1(
527                &format!(
528                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
529                    &self.scene_graph.sketch_mode
530                )
531                .into(),
532            );
533        }
534        self.scene_graph.sketch_mode = None;
535
536        // Execute.
537        let outcome = ctx.run_with_caching(self.program.clone()).await?;
538
539        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
540        self.update_state_after_exec(outcome, false);
541
542        Ok(self.scene_graph.clone())
543    }
544
545    async fn delete_sketch(
546        &mut self,
547        ctx: &ExecutorContext,
548        _version: Version,
549        sketch: ObjectId,
550    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
551        // TODO: Check version.
552
553        let mut new_ast = self.program.ast.clone();
554
555        // Look up existing sketch.
556        let sketch_id = sketch;
557        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
558            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
559        })?;
560        let ObjectKind::Sketch(_) = &sketch_object.kind else {
561            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
562                "Object is not a sketch, it is {}",
563                sketch_object.kind.human_friendly_kind_with_article(),
564            ))));
565        };
566
567        // Modify the AST to remove the sketch.
568        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
569            .map_err(KclErrorWithOutputs::no_outputs)?;
570
571        self.execute_after_delete_sketch(ctx, &mut new_ast).await
572    }
573
574    async fn add_segment(
575        &mut self,
576        ctx: &ExecutorContext,
577        _version: Version,
578        sketch: ObjectId,
579        segment: SegmentCtor,
580        _label: Option<String>,
581    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
582        // TODO: Check version.
583        match segment {
584            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
585            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
586            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
587            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
588        }
589    }
590
591    async fn edit_segments(
592        &mut self,
593        ctx: &ExecutorContext,
594        _version: Version,
595        sketch: ObjectId,
596        segments: Vec<ExistingSegmentCtor>,
597    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
598        // TODO: Check version.
599        let sketch_block_ref =
600            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
601
602        let mut new_ast = self.program.ast.clone();
603        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
604
605        // segment_ids_edited still has to be the original segments (not final_edits), otherwise the owner segments
606        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
607        for segment in &segments {
608            segment_ids_edited.insert(segment.id);
609        }
610
611        // Preprocess segments into a final_edits vector to handle if segments contains:
612        // - edit start point of line1 (as SegmentCtor::Point)
613        // - edit end point of line1 (as SegmentCtor::Point)
614        //
615        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
616        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
617        //
618        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
619        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
620        // so the above example would result in a single line1 edit:
621        // - the first start point edit creates a new line edit entry in final_edits
622        // - the second end point edit finds this line edit and mutates the end position only.
623        //
624        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
625        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
626
627        for segment in segments {
628            let segment_id = segment.id;
629            match segment.ctor {
630                SegmentCtor::Point(ctor) => {
631                    // Find the owner, if any (point -> line / arc)
632                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
633                        && let ObjectKind::Segment { segment } = &segment_object.kind
634                        && let Segment::Point(point) = segment
635                        && let Some(owner_id) = point.owner
636                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
637                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
638                    {
639                        match owner_segment {
640                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
641                                if let Some(existing) = final_edits.get_mut(&owner_id) {
642                                    let SegmentCtor::Line(line_ctor) = existing else {
643                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
644                                            "Internal: Expected line ctor for owner, but found {}",
645                                            existing.human_friendly_kind_with_article()
646                                        ))));
647                                    };
648                                    // Line owner is already in final_edits -> apply this point edit
649                                    if line.start == segment_id {
650                                        line_ctor.start = ctor.position;
651                                    } else {
652                                        line_ctor.end = ctor.position;
653                                    }
654                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
655                                    // Line owner is not in final_edits yet -> create it
656                                    let mut line_ctor = line_ctor.clone();
657                                    if line.start == segment_id {
658                                        line_ctor.start = ctor.position;
659                                    } else {
660                                        line_ctor.end = ctor.position;
661                                    }
662                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
663                                } else {
664                                    // This should never run..
665                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
666                                        "Internal: Line does not have line ctor, but found {}",
667                                        line.ctor.human_friendly_kind_with_article()
668                                    ))));
669                                }
670                                continue;
671                            }
672                            Segment::Arc(arc)
673                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
674                            {
675                                if let Some(existing) = final_edits.get_mut(&owner_id) {
676                                    let SegmentCtor::Arc(arc_ctor) = existing else {
677                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
678                                            "Internal: Expected arc ctor for owner, but found {}",
679                                            existing.human_friendly_kind_with_article()
680                                        ))));
681                                    };
682                                    if arc.start == segment_id {
683                                        arc_ctor.start = ctor.position;
684                                    } else if arc.end == segment_id {
685                                        arc_ctor.end = ctor.position;
686                                    } else {
687                                        arc_ctor.center = ctor.position;
688                                    }
689                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
690                                    let mut arc_ctor = arc_ctor.clone();
691                                    if arc.start == segment_id {
692                                        arc_ctor.start = ctor.position;
693                                    } else if arc.end == segment_id {
694                                        arc_ctor.end = ctor.position;
695                                    } else {
696                                        arc_ctor.center = ctor.position;
697                                    }
698                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
699                                } else {
700                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
701                                        "Internal: Arc does not have arc ctor, but found {}",
702                                        arc.ctor.human_friendly_kind_with_article()
703                                    ))));
704                                }
705                                continue;
706                            }
707                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
708                                if let Some(existing) = final_edits.get_mut(&owner_id) {
709                                    let SegmentCtor::Circle(circle_ctor) = existing else {
710                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
711                                            "Internal: Expected circle ctor for owner, but found {}",
712                                            existing.human_friendly_kind_with_article()
713                                        ))));
714                                    };
715                                    if circle.start == segment_id {
716                                        circle_ctor.start = ctor.position;
717                                    } else {
718                                        circle_ctor.center = ctor.position;
719                                    }
720                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
721                                    let mut circle_ctor = circle_ctor.clone();
722                                    if circle.start == segment_id {
723                                        circle_ctor.start = ctor.position;
724                                    } else {
725                                        circle_ctor.center = ctor.position;
726                                    }
727                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
728                                } else {
729                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
730                                        "Internal: Circle does not have circle ctor, but found {}",
731                                        circle.ctor.human_friendly_kind_with_article()
732                                    ))));
733                                }
734                                continue;
735                            }
736                            _ => {}
737                        }
738                    }
739
740                    // No owner, it's an individual point
741                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
742                }
743                SegmentCtor::Line(ctor) => {
744                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
745                }
746                SegmentCtor::Arc(ctor) => {
747                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
748                }
749                SegmentCtor::Circle(ctor) => {
750                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
751                }
752            }
753        }
754
755        for (segment_id, ctor) in final_edits {
756            match ctor {
757                SegmentCtor::Point(ctor) => self
758                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
759                    .map_err(KclErrorWithOutputs::no_outputs)?,
760                SegmentCtor::Line(ctor) => self
761                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
762                    .map_err(KclErrorWithOutputs::no_outputs)?,
763                SegmentCtor::Arc(ctor) => self
764                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
765                    .map_err(KclErrorWithOutputs::no_outputs)?,
766                SegmentCtor::Circle(ctor) => self
767                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
768                    .map_err(KclErrorWithOutputs::no_outputs)?,
769            }
770        }
771        self.execute_after_edit(
772            ctx,
773            sketch,
774            sketch_block_ref,
775            segment_ids_edited,
776            EditDeleteKind::Edit,
777            &mut new_ast,
778        )
779        .await
780    }
781
782    async fn delete_objects(
783        &mut self,
784        ctx: &ExecutorContext,
785        _version: Version,
786        sketch: ObjectId,
787        constraint_ids: Vec<ObjectId>,
788        segment_ids: Vec<ObjectId>,
789    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
790        // TODO: Check version.
791        let sketch_block_ref =
792            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
793
794        // Deduplicate IDs.
795        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
796        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
797
798        // If a point is owned by a Line/Arc, we want to delete the owner, which will
799        // also delete the point, as well as other points that are owned by the owner.
800        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
801
802        for segment_id in segment_ids_set.iter().copied() {
803            if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
804                && let ObjectKind::Segment { segment } = &segment_object.kind
805                && let Segment::Point(point) = segment
806                && let Some(owner_id) = point.owner
807                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
808                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
809                && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_))
810            {
811                // segment is owned -> delete the owner
812                resolved_segment_ids_to_delete.insert(owner_id);
813            } else {
814                // segment is not owned by anything -> can be deleted
815                resolved_segment_ids_to_delete.insert(segment_id);
816            }
817        }
818        let referenced_constraint_ids = self
819            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
820            .map_err(KclErrorWithOutputs::no_outputs)?;
821
822        let mut new_ast = self.program.ast.clone();
823
824        for constraint_id in referenced_constraint_ids {
825            if constraint_ids_set.contains(&constraint_id) {
826                continue;
827            }
828
829            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
830                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
831            })?;
832            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
833                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
834                    "Object is not a constraint, it is {}",
835                    constraint_object.kind.human_friendly_kind_with_article()
836                ))));
837            };
838
839            match constraint {
840                Constraint::Coincident(coincident) => {
841                    let remaining_segments =
842                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
843
844                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
845                    if remaining_segments.len() >= 2 {
846                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
847                            .map_err(KclErrorWithOutputs::no_outputs)?;
848                    } else {
849                        constraint_ids_set.insert(constraint_id);
850                    }
851                }
852                Constraint::EqualRadius(equal_radius) => {
853                    let remaining_input = equal_radius
854                        .input
855                        .iter()
856                        .copied()
857                        .filter(|segment_id| {
858                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
859                        })
860                        .collect::<Vec<_>>();
861
862                    if remaining_input.len() >= 2 {
863                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
864                            .map_err(KclErrorWithOutputs::no_outputs)?;
865                    } else {
866                        constraint_ids_set.insert(constraint_id);
867                    }
868                }
869                Constraint::LinesEqualLength(lines_equal_length) => {
870                    let remaining_lines = lines_equal_length
871                        .lines
872                        .iter()
873                        .copied()
874                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
875                        .collect::<Vec<_>>();
876
877                    // Equal length constraint is only valid with at least 2 lines
878                    if remaining_lines.len() >= 2 {
879                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
880                            .map_err(KclErrorWithOutputs::no_outputs)?;
881                    } else {
882                        constraint_ids_set.insert(constraint_id);
883                    }
884                }
885                Constraint::Parallel(parallel) => {
886                    let remaining_lines = parallel
887                        .lines
888                        .iter()
889                        .copied()
890                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
891                        .collect::<Vec<_>>();
892
893                    if remaining_lines.len() >= 2 {
894                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
895                            .map_err(KclErrorWithOutputs::no_outputs)?;
896                    } else {
897                        constraint_ids_set.insert(constraint_id);
898                    }
899                }
900                Constraint::Horizontal(Horizontal::Points { points }) => {
901                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
902
903                    if remaining_points.len() >= 2 {
904                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
905                            .map_err(KclErrorWithOutputs::no_outputs)?;
906                    } else {
907                        constraint_ids_set.insert(constraint_id);
908                    }
909                }
910                Constraint::Vertical(Vertical::Points { points }) => {
911                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
912
913                    if remaining_points.len() >= 2 {
914                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
915                            .map_err(KclErrorWithOutputs::no_outputs)?;
916                    } else {
917                        constraint_ids_set.insert(constraint_id);
918                    }
919                }
920                Constraint::Fixed(fixed) => {
921                    if fixed.points.iter().any(|fixed_point| {
922                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
923                    }) {
924                        constraint_ids_set.insert(constraint_id);
925                    }
926                }
927                _ => {
928                    // All other constraint types: if referenced by a segment -> delete the constraint
929                    constraint_ids_set.insert(constraint_id);
930                }
931            }
932        }
933
934        for constraint_id in constraint_ids_set {
935            self.delete_constraint(&mut new_ast, sketch, constraint_id)
936                .map_err(KclErrorWithOutputs::no_outputs)?;
937        }
938        for segment_id in resolved_segment_ids_to_delete {
939            self.delete_segment(&mut new_ast, sketch, segment_id)
940                .map_err(KclErrorWithOutputs::no_outputs)?;
941        }
942
943        self.execute_after_edit(
944            ctx,
945            sketch,
946            sketch_block_ref,
947            Default::default(),
948            EditDeleteKind::DeleteNonSketch,
949            &mut new_ast,
950        )
951        .await
952    }
953
954    async fn add_constraint(
955        &mut self,
956        ctx: &ExecutorContext,
957        _version: Version,
958        sketch: ObjectId,
959        constraint: Constraint,
960    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
961        // TODO: Check version.
962
963        // Save the original state as a backup - we'll restore it if anything fails
964        let original_program = self.program.clone();
965        let original_scene_graph = self.scene_graph.clone();
966
967        let mut new_ast = self.program.ast.clone();
968        let sketch_block_ref = match constraint {
969            Constraint::Coincident(coincident) => self
970                .add_coincident(sketch, coincident, &mut new_ast)
971                .await
972                .map_err(KclErrorWithOutputs::no_outputs)?,
973            Constraint::Distance(distance) => self
974                .add_distance(sketch, distance, &mut new_ast)
975                .await
976                .map_err(KclErrorWithOutputs::no_outputs)?,
977            Constraint::EqualRadius(equal_radius) => self
978                .add_equal_radius(sketch, equal_radius, &mut new_ast)
979                .await
980                .map_err(KclErrorWithOutputs::no_outputs)?,
981            Constraint::Fixed(fixed) => self
982                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
983                .await
984                .map_err(KclErrorWithOutputs::no_outputs)?,
985            Constraint::HorizontalDistance(distance) => self
986                .add_horizontal_distance(sketch, distance, &mut new_ast)
987                .await
988                .map_err(KclErrorWithOutputs::no_outputs)?,
989            Constraint::VerticalDistance(distance) => self
990                .add_vertical_distance(sketch, distance, &mut new_ast)
991                .await
992                .map_err(KclErrorWithOutputs::no_outputs)?,
993            Constraint::Horizontal(horizontal) => self
994                .add_horizontal(sketch, horizontal, &mut new_ast)
995                .await
996                .map_err(KclErrorWithOutputs::no_outputs)?,
997            Constraint::LinesEqualLength(lines_equal_length) => self
998                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
999                .await
1000                .map_err(KclErrorWithOutputs::no_outputs)?,
1001            Constraint::Midpoint(midpoint) => self
1002                .add_midpoint(sketch, midpoint, &mut new_ast)
1003                .await
1004                .map_err(KclErrorWithOutputs::no_outputs)?,
1005            Constraint::Parallel(parallel) => self
1006                .add_parallel(sketch, parallel, &mut new_ast)
1007                .await
1008                .map_err(KclErrorWithOutputs::no_outputs)?,
1009            Constraint::Perpendicular(perpendicular) => self
1010                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1011                .await
1012                .map_err(KclErrorWithOutputs::no_outputs)?,
1013            Constraint::Radius(radius) => self
1014                .add_radius(sketch, radius, &mut new_ast)
1015                .await
1016                .map_err(KclErrorWithOutputs::no_outputs)?,
1017            Constraint::Diameter(diameter) => self
1018                .add_diameter(sketch, diameter, &mut new_ast)
1019                .await
1020                .map_err(KclErrorWithOutputs::no_outputs)?,
1021            Constraint::Symmetric(symmetric) => self
1022                .add_symmetric(sketch, symmetric, &mut new_ast)
1023                .await
1024                .map_err(KclErrorWithOutputs::no_outputs)?,
1025            Constraint::Vertical(vertical) => self
1026                .add_vertical(sketch, vertical, &mut new_ast)
1027                .await
1028                .map_err(KclErrorWithOutputs::no_outputs)?,
1029            Constraint::Angle(lines_at_angle) => self
1030                .add_angle(sketch, lines_at_angle, &mut new_ast)
1031                .await
1032                .map_err(KclErrorWithOutputs::no_outputs)?,
1033            Constraint::Tangent(tangent) => self
1034                .add_tangent(sketch, tangent, &mut new_ast)
1035                .await
1036                .map_err(KclErrorWithOutputs::no_outputs)?,
1037        };
1038
1039        let result = self
1040            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1041            .await;
1042
1043        // If execution failed, restore the original state to prevent corruption
1044        if result.is_err() {
1045            self.program = original_program;
1046            self.scene_graph = original_scene_graph;
1047        }
1048
1049        result
1050    }
1051
1052    async fn chain_segment(
1053        &mut self,
1054        ctx: &ExecutorContext,
1055        version: Version,
1056        sketch: ObjectId,
1057        previous_segment_end_point_id: ObjectId,
1058        segment: SegmentCtor,
1059        _label: Option<String>,
1060    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1061        // TODO: Check version.
1062
1063        // First, add the segment (line) to get its start point ID
1064        let SegmentCtor::Line(line_ctor) = segment else {
1065            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1066                "chain_segment currently only supports Line segments, got {}",
1067                segment.human_friendly_kind_with_article(),
1068            ))));
1069        };
1070
1071        // Add the line segment first - this updates self.program and self.scene_graph
1072        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1073
1074        // Find the new line's start point ID from the updated scene graph
1075        // add_line updates self.scene_graph, so we can use that
1076        let new_line_id = first_scene_delta
1077            .new_objects
1078            .iter()
1079            .find(|&obj_id| {
1080                let obj = self.scene_graph.objects.get(obj_id.0);
1081                if let Some(obj) = obj {
1082                    matches!(
1083                        &obj.kind,
1084                        ObjectKind::Segment {
1085                            segment: Segment::Line(_)
1086                        }
1087                    )
1088                } else {
1089                    false
1090                }
1091            })
1092            .ok_or_else(|| {
1093                KclErrorWithOutputs::no_outputs(KclError::refactor(
1094                    "Failed to find new line segment in scene graph".to_string(),
1095                ))
1096            })?;
1097
1098        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1099            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1100                "New line object not found: {new_line_id:?}"
1101            )))
1102        })?;
1103
1104        let ObjectKind::Segment {
1105            segment: new_line_segment,
1106        } = &new_line_obj.kind
1107        else {
1108            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1109                "Object is not a segment: {new_line_obj:?}"
1110            ))));
1111        };
1112
1113        let Segment::Line(new_line) = new_line_segment else {
1114            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1115                "Segment is not a line: {new_line_segment:?}"
1116            ))));
1117        };
1118
1119        let new_line_start_point_id = new_line.start;
1120
1121        // Now add the coincident constraint between the previous end point and the new line's start point.
1122        let coincident = Coincident {
1123            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1124        };
1125
1126        let (final_src_delta, final_scene_delta) = self
1127            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1128            .await?;
1129
1130        // Combine new objects from the line addition and the constraint addition.
1131        // Both add_line and add_constraint now populate new_objects correctly.
1132        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1133        combined_new_objects.extend(final_scene_delta.new_objects);
1134
1135        let scene_graph_delta = SceneGraphDelta {
1136            new_graph: self.scene_graph.clone(),
1137            invalidates_ids: false,
1138            new_objects: combined_new_objects,
1139            exec_outcome: final_scene_delta.exec_outcome,
1140        };
1141
1142        Ok((final_src_delta, scene_graph_delta))
1143    }
1144
1145    async fn edit_constraint(
1146        &mut self,
1147        ctx: &ExecutorContext,
1148        _version: Version,
1149        sketch: ObjectId,
1150        constraint_id: ObjectId,
1151        value_expression: String,
1152    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1153        // TODO: Check version.
1154        let sketch_block_ref =
1155            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1156
1157        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1158            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1159        })?;
1160        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1161            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1162                "Object is not a constraint: {constraint_id:?}"
1163            ))));
1164        }
1165
1166        let mut new_ast = self.program.ast.clone();
1167
1168        // Parse the expression string into an AST node.
1169        let (parsed, errors) = Program::parse(&value_expression)
1170            .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1171        if !errors.is_empty() {
1172            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1173                "Error parsing value expression: {errors:?}"
1174            ))));
1175        }
1176        let mut parsed = parsed.ok_or_else(|| {
1177            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1178        })?;
1179        if parsed.ast.body.is_empty() {
1180            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1181                "Empty value expression".to_string(),
1182            )));
1183        }
1184        let first = parsed.ast.body.remove(0);
1185        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1186            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1187                "Value expression must be a simple expression".to_string(),
1188            )));
1189        };
1190
1191        let new_value: ast::BinaryPart = expr_stmt
1192            .inner
1193            .expression
1194            .try_into()
1195            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1196
1197        self.mutate_ast(
1198            &mut new_ast,
1199            constraint_id,
1200            AstMutateCommand::EditConstraintValue { value: new_value },
1201        )
1202        .map_err(KclErrorWithOutputs::no_outputs)?;
1203
1204        self.execute_after_edit(
1205            ctx,
1206            sketch,
1207            sketch_block_ref,
1208            Default::default(),
1209            EditDeleteKind::Edit,
1210            &mut new_ast,
1211        )
1212        .await
1213    }
1214
1215    async fn edit_distance_constraint_label_position(
1216        &mut self,
1217        ctx: &ExecutorContext,
1218        _version: Version,
1219        sketch: ObjectId,
1220        constraint_id: ObjectId,
1221        label_position: Point2d<Number>,
1222        anchor_segment_ids: Vec<ObjectId>,
1223    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1224        // TODO: Check version.
1225        let sketch_block_ref =
1226            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1227
1228        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1229            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1230        })?;
1231        if !matches!(
1232            &object.kind,
1233            ObjectKind::Constraint {
1234                constraint: Constraint::Distance(_)
1235                    | Constraint::HorizontalDistance(_)
1236                    | Constraint::VerticalDistance(_)
1237                    | Constraint::Radius(_)
1238                    | Constraint::Diameter(_),
1239            }
1240        ) {
1241            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1242                "Object does not support labelPosition: {constraint_id:?}"
1243            ))));
1244        }
1245
1246        let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1247            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1248                "Could not convert label position to AST: {err}"
1249            )))
1250        })?;
1251        let mut new_ast = self.program.ast.clone();
1252        self.mutate_ast(
1253            &mut new_ast,
1254            constraint_id,
1255            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1256        )
1257        .map_err(KclErrorWithOutputs::no_outputs)?;
1258
1259        self.execute_after_edit(
1260            ctx,
1261            sketch,
1262            sketch_block_ref,
1263            anchor_segment_ids.into_iter().collect(),
1264            EditDeleteKind::Edit,
1265            &mut new_ast,
1266        )
1267        .await
1268    }
1269
1270    /// Splitting a segment means creating a new segment, editing the old one, and then
1271    /// migrating a bunch of the constraints from the original segment to the new one
1272    /// (i.e. deleting them and re-adding them on the other segment).
1273    ///
1274    /// To keep this efficient we require as few executions as possible: we create the
1275    /// new segment first (to get its id), then do all edits and new constraints, and
1276    /// do all deletes at the end (since deletes invalidate ids).
1277    async fn batch_split_segment_operations(
1278        &mut self,
1279        ctx: &ExecutorContext,
1280        _version: Version,
1281        sketch: ObjectId,
1282        edit_segments: Vec<ExistingSegmentCtor>,
1283        add_constraints: Vec<Constraint>,
1284        delete_constraint_ids: Vec<ObjectId>,
1285        _new_segment_info: sketch::NewSegmentInfo,
1286    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1287        // TODO: Check version.
1288        let sketch_block_ref =
1289            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1290
1291        let mut new_ast = self.program.ast.clone();
1292        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1293
1294        // Step 1: Edit segments
1295        for segment in edit_segments {
1296            segment_ids_edited.insert(segment.id);
1297            match segment.ctor {
1298                SegmentCtor::Point(ctor) => self
1299                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1300                    .map_err(KclErrorWithOutputs::no_outputs)?,
1301                SegmentCtor::Line(ctor) => self
1302                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1303                    .map_err(KclErrorWithOutputs::no_outputs)?,
1304                SegmentCtor::Arc(ctor) => self
1305                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1306                    .map_err(KclErrorWithOutputs::no_outputs)?,
1307                SegmentCtor::Circle(ctor) => self
1308                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1309                    .map_err(KclErrorWithOutputs::no_outputs)?,
1310            }
1311        }
1312
1313        // Step 2: Add all constraints
1314        for constraint in add_constraints {
1315            match constraint {
1316                Constraint::Coincident(coincident) => {
1317                    self.add_coincident(sketch, coincident, &mut new_ast)
1318                        .await
1319                        .map_err(KclErrorWithOutputs::no_outputs)?;
1320                }
1321                Constraint::Distance(distance) => {
1322                    self.add_distance(sketch, distance, &mut new_ast)
1323                        .await
1324                        .map_err(KclErrorWithOutputs::no_outputs)?;
1325                }
1326                Constraint::EqualRadius(equal_radius) => {
1327                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1328                        .await
1329                        .map_err(KclErrorWithOutputs::no_outputs)?;
1330                }
1331                Constraint::Fixed(fixed) => {
1332                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1333                        .await
1334                        .map_err(KclErrorWithOutputs::no_outputs)?;
1335                }
1336                Constraint::HorizontalDistance(distance) => {
1337                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1338                        .await
1339                        .map_err(KclErrorWithOutputs::no_outputs)?;
1340                }
1341                Constraint::VerticalDistance(distance) => {
1342                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1343                        .await
1344                        .map_err(KclErrorWithOutputs::no_outputs)?;
1345                }
1346                Constraint::Horizontal(horizontal) => {
1347                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1348                        .await
1349                        .map_err(KclErrorWithOutputs::no_outputs)?;
1350                }
1351                Constraint::LinesEqualLength(lines_equal_length) => {
1352                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1353                        .await
1354                        .map_err(KclErrorWithOutputs::no_outputs)?;
1355                }
1356                Constraint::Midpoint(midpoint) => {
1357                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1358                        .await
1359                        .map_err(KclErrorWithOutputs::no_outputs)?;
1360                }
1361                Constraint::Parallel(parallel) => {
1362                    self.add_parallel(sketch, parallel, &mut new_ast)
1363                        .await
1364                        .map_err(KclErrorWithOutputs::no_outputs)?;
1365                }
1366                Constraint::Perpendicular(perpendicular) => {
1367                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1368                        .await
1369                        .map_err(KclErrorWithOutputs::no_outputs)?;
1370                }
1371                Constraint::Vertical(vertical) => {
1372                    self.add_vertical(sketch, vertical, &mut new_ast)
1373                        .await
1374                        .map_err(KclErrorWithOutputs::no_outputs)?;
1375                }
1376                Constraint::Diameter(diameter) => {
1377                    self.add_diameter(sketch, diameter, &mut new_ast)
1378                        .await
1379                        .map_err(KclErrorWithOutputs::no_outputs)?;
1380                }
1381                Constraint::Radius(radius) => {
1382                    self.add_radius(sketch, radius, &mut new_ast)
1383                        .await
1384                        .map_err(KclErrorWithOutputs::no_outputs)?;
1385                }
1386                Constraint::Symmetric(symmetric) => {
1387                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1388                        .await
1389                        .map_err(KclErrorWithOutputs::no_outputs)?;
1390                }
1391                Constraint::Angle(angle) => {
1392                    self.add_angle(sketch, angle, &mut new_ast)
1393                        .await
1394                        .map_err(KclErrorWithOutputs::no_outputs)?;
1395                }
1396                Constraint::Tangent(tangent) => {
1397                    self.add_tangent(sketch, tangent, &mut new_ast)
1398                        .await
1399                        .map_err(KclErrorWithOutputs::no_outputs)?;
1400                }
1401            }
1402        }
1403
1404        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1405        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1406
1407        let has_constraint_deletions = !constraint_ids_set.is_empty();
1408        for constraint_id in constraint_ids_set {
1409            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1410                .map_err(KclErrorWithOutputs::no_outputs)?;
1411        }
1412
1413        // Step 4: Execute once at the end
1414        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1415        // But we'll manually set invalidates_ids: true if we deleted constraints
1416        let (source_delta, mut scene_graph_delta) = self
1417            .execute_after_edit(
1418                ctx,
1419                sketch,
1420                sketch_block_ref,
1421                segment_ids_edited,
1422                EditDeleteKind::Edit,
1423                &mut new_ast,
1424            )
1425            .await?;
1426
1427        // If we deleted constraints, set invalidates_ids: true
1428        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1429        if has_constraint_deletions {
1430            scene_graph_delta.invalidates_ids = true;
1431        }
1432
1433        Ok((source_delta, scene_graph_delta))
1434    }
1435
1436    async fn batch_tail_cut_operations(
1437        &mut self,
1438        ctx: &ExecutorContext,
1439        _version: Version,
1440        sketch: ObjectId,
1441        edit_segments: Vec<ExistingSegmentCtor>,
1442        add_constraints: Vec<Constraint>,
1443        delete_constraint_ids: Vec<ObjectId>,
1444    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1445        let sketch_block_ref =
1446            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1447
1448        let mut new_ast = self.program.ast.clone();
1449        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1450
1451        // Step 1: Edit segments (usually a single segment for tail cut)
1452        for segment in edit_segments {
1453            segment_ids_edited.insert(segment.id);
1454            match segment.ctor {
1455                SegmentCtor::Point(ctor) => self
1456                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1457                    .map_err(KclErrorWithOutputs::no_outputs)?,
1458                SegmentCtor::Line(ctor) => self
1459                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1460                    .map_err(KclErrorWithOutputs::no_outputs)?,
1461                SegmentCtor::Arc(ctor) => self
1462                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1463                    .map_err(KclErrorWithOutputs::no_outputs)?,
1464                SegmentCtor::Circle(ctor) => self
1465                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1466                    .map_err(KclErrorWithOutputs::no_outputs)?,
1467            }
1468        }
1469
1470        // Step 2: Add coincident constraints
1471        for constraint in add_constraints {
1472            match constraint {
1473                Constraint::Coincident(coincident) => {
1474                    self.add_coincident(sketch, coincident, &mut new_ast)
1475                        .await
1476                        .map_err(KclErrorWithOutputs::no_outputs)?;
1477                }
1478                other => {
1479                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1480                        "unsupported constraint in tail cut batch: {other:?}"
1481                    ))));
1482                }
1483            }
1484        }
1485
1486        // Step 3: Delete constraints (if any)
1487        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1488
1489        let has_constraint_deletions = !constraint_ids_set.is_empty();
1490        for constraint_id in constraint_ids_set {
1491            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1492                .map_err(KclErrorWithOutputs::no_outputs)?;
1493        }
1494
1495        // Step 4: Single execute_after_edit
1496        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1497        // But we'll manually set invalidates_ids: true if we deleted constraints
1498        let (source_delta, mut scene_graph_delta) = self
1499            .execute_after_edit(
1500                ctx,
1501                sketch,
1502                sketch_block_ref,
1503                segment_ids_edited,
1504                EditDeleteKind::Edit,
1505                &mut new_ast,
1506            )
1507            .await?;
1508
1509        // If we deleted constraints, set invalidates_ids: true
1510        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1511        if has_constraint_deletions {
1512            scene_graph_delta.invalidates_ids = true;
1513        }
1514
1515        Ok((source_delta, scene_graph_delta))
1516    }
1517}
1518
1519impl FrontendState {
1520    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1521        self.program = program.clone();
1522
1523        // Execute so that the objects are updated and available for the next
1524        // API call.
1525        // This always uses engine execution (not mock) so that things are cached.
1526        // Engine execution now runs freedom analysis automatically.
1527        // Keep existing checkpoints alive here. History may still reference
1528        // older committed sketch states across a direct-edit boundary, and a
1529        // checkpoint restore is a full state replacement anyway. We append a
1530        // fresh baseline checkpoint after the full execution below.
1531        // Clear the freedom cache since IDs might have changed after direct editing
1532        // and we're about to run freedom analysis which will repopulate it.
1533        self.point_freedom_cache.clear();
1534        match ctx.run_with_caching(program).await {
1535            Ok(outcome) => {
1536                let outcome = self.update_state_after_exec(outcome, true);
1537                let checkpoint_id = self
1538                    .create_sketch_checkpoint(outcome.clone())
1539                    .await
1540                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1541                Ok(SetProgramOutcome::Success {
1542                    scene_graph: Box::new(self.scene_graph.clone()),
1543                    exec_outcome: Box::new(outcome),
1544                    checkpoint_id: Some(checkpoint_id),
1545                })
1546            }
1547            Err(mut err) => {
1548                // Don't return an error just because execution failed. Instead,
1549                // update state as much as possible.
1550                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1551                self.update_state_after_exec(outcome, true);
1552                err.scene_graph = Some(self.scene_graph.clone());
1553                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1554            }
1555        }
1556    }
1557
1558    /// Decorate engine execution such that our state is updated and the scene
1559    /// graph is added to the return.
1560    pub async fn engine_execute(
1561        &mut self,
1562        ctx: &ExecutorContext,
1563        program: Program,
1564    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1565        self.program = program.clone();
1566
1567        // Engine execution now runs freedom analysis automatically. Clear the
1568        // freedom cache since IDs might have changed after direct editing, and
1569        // we're about to run freedom analysis which will repopulate it.
1570        self.point_freedom_cache.clear();
1571        match ctx.run_with_caching(program).await {
1572            Ok(outcome) => {
1573                let outcome = self.update_state_after_exec(outcome, true);
1574                Ok(SceneGraphDelta {
1575                    new_graph: self.scene_graph.clone(),
1576                    exec_outcome: outcome,
1577                    // We don't know what the new objects are.
1578                    new_objects: Default::default(),
1579                    // We don't know if IDs were invalidated.
1580                    invalidates_ids: Default::default(),
1581                })
1582            }
1583            Err(mut err) => {
1584                // Update state as much as possible, even when there's an error.
1585                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1586                self.update_state_after_exec(outcome, true);
1587                err.scene_graph = Some(self.scene_graph.clone());
1588                Err(err)
1589            }
1590        }
1591    }
1592
1593    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1594        if matches!(err.error, KclError::EngineHangup { .. }) {
1595            // It's not ideal to special-case this, but this error is very
1596            // common during development, and it causes confusing downstream
1597            // errors that have nothing to do with the actual problem.
1598            return Err(err);
1599        }
1600
1601        let KclErrorWithOutputs {
1602            error,
1603            mut non_fatal,
1604            variables,
1605            #[cfg(feature = "artifact-graph")]
1606            operations,
1607            #[cfg(feature = "artifact-graph")]
1608            artifact_graph,
1609            #[cfg(feature = "artifact-graph")]
1610            scene_objects,
1611            #[cfg(feature = "artifact-graph")]
1612            source_range_to_object,
1613            #[cfg(feature = "artifact-graph")]
1614            var_solutions,
1615            filenames,
1616            default_planes,
1617            ..
1618        } = err;
1619
1620        if let Some(source_range) = error.source_ranges().first() {
1621            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1622        } else {
1623            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1624        }
1625
1626        Ok(ExecOutcome {
1627            variables,
1628            filenames,
1629            #[cfg(feature = "artifact-graph")]
1630            operations,
1631            #[cfg(feature = "artifact-graph")]
1632            artifact_graph,
1633            #[cfg(feature = "artifact-graph")]
1634            scene_objects,
1635            #[cfg(feature = "artifact-graph")]
1636            source_range_to_object,
1637            #[cfg(feature = "artifact-graph")]
1638            var_solutions,
1639            issues: non_fatal,
1640            default_planes,
1641        })
1642    }
1643
1644    async fn add_point(
1645        &mut self,
1646        ctx: &ExecutorContext,
1647        sketch: ObjectId,
1648        ctor: PointCtor,
1649    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1650        // Create updated KCL source from args.
1651        let at_ast = to_ast_point2d(&ctor.position)
1652            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1653        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1654            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1655            unlabeled: None,
1656            arguments: vec![ast::LabeledArg {
1657                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1658                arg: at_ast,
1659            }],
1660            digest: None,
1661            non_code_meta: Default::default(),
1662        })));
1663
1664        // Look up existing sketch.
1665        let sketch_id = sketch;
1666        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1667            #[cfg(target_arch = "wasm32")]
1668            web_sys::console::error_1(
1669                &format!(
1670                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1671                    &self.scene_graph.objects
1672                )
1673                .into(),
1674            );
1675            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1676        })?;
1677        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1678            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1679                "Object is not a sketch, it is {}",
1680                sketch_object.kind.human_friendly_kind_with_article(),
1681            ))));
1682        };
1683        // Add the point to the AST of the sketch block.
1684        let mut new_ast = self.program.ast.clone();
1685        let (sketch_block_ref, _) = self
1686            .mutate_ast(
1687                &mut new_ast,
1688                sketch_id,
1689                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1690            )
1691            .map_err(KclErrorWithOutputs::no_outputs)?;
1692        // Convert to string source to create real source ranges.
1693        let new_source = source_from_ast(&new_ast);
1694        // Parse the new KCL source.
1695        let (new_program, errors) = Program::parse(&new_source)
1696            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1697        if !errors.is_empty() {
1698            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1699                "Error parsing KCL source after adding point: {errors:?}"
1700            ))));
1701        }
1702        let Some(new_program) = new_program else {
1703            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1704                "No AST produced after adding point".to_string(),
1705            )));
1706        };
1707
1708        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1709            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1710                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1711            )))
1712        })?;
1713        #[cfg(not(feature = "artifact-graph"))]
1714        let _ = point_node_ref;
1715
1716        // Make sure to only set this if there are no errors.
1717        self.program = new_program.clone();
1718
1719        // Truncate after the sketch block for mock execution.
1720        let mut truncated_program = new_program;
1721        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1722            .map_err(KclErrorWithOutputs::no_outputs)?;
1723
1724        // Execute.
1725        let outcome = ctx
1726            .run_mock(
1727                &truncated_program,
1728                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1729            )
1730            .await?;
1731
1732        #[cfg(not(feature = "artifact-graph"))]
1733        let new_object_ids = Vec::new();
1734        #[cfg(feature = "artifact-graph")]
1735        let new_object_ids = {
1736            let make_err =
1737                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1738            let segment_id = outcome
1739                .source_range_to_object
1740                .get(&point_node_ref.range)
1741                .copied()
1742                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1743            let segment_object = outcome
1744                .scene_objects
1745                .get(segment_id.0)
1746                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1747            let ObjectKind::Segment { segment } = &segment_object.kind else {
1748                return Err(make_err(format!(
1749                    "Object is not a segment, it is {}",
1750                    segment_object.kind.human_friendly_kind_with_article()
1751                )));
1752            };
1753            let Segment::Point(_) = segment else {
1754                return Err(make_err(format!(
1755                    "Segment is not a point, it is {}",
1756                    segment.human_friendly_kind_with_article()
1757                )));
1758            };
1759            vec![segment_id]
1760        };
1761        let src_delta = SourceDelta { text: new_source };
1762        // Uses .no_freedom_analysis() so freedom_analysis: false
1763        let outcome = self.update_state_after_exec(outcome, false);
1764        let scene_graph_delta = SceneGraphDelta {
1765            new_graph: self.scene_graph.clone(),
1766            invalidates_ids: false,
1767            new_objects: new_object_ids,
1768            exec_outcome: outcome,
1769        };
1770        Ok((src_delta, scene_graph_delta))
1771    }
1772
1773    async fn add_line(
1774        &mut self,
1775        ctx: &ExecutorContext,
1776        sketch: ObjectId,
1777        ctor: LineCtor,
1778    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1779        // Create updated KCL source from args.
1780        let start_ast = to_ast_point2d(&ctor.start)
1781            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1782        let end_ast = to_ast_point2d(&ctor.end)
1783            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1784        let mut arguments = vec![
1785            ast::LabeledArg {
1786                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1787                arg: start_ast,
1788            },
1789            ast::LabeledArg {
1790                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1791                arg: end_ast,
1792            },
1793        ];
1794        // Add construction kwarg if construction is Some(true)
1795        if ctor.construction == Some(true) {
1796            arguments.push(ast::LabeledArg {
1797                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1798                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1799                    value: ast::LiteralValue::Bool(true),
1800                    raw: "true".to_string(),
1801                    digest: None,
1802                }))),
1803            });
1804        }
1805        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1806            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1807            unlabeled: None,
1808            arguments,
1809            digest: None,
1810            non_code_meta: Default::default(),
1811        })));
1812
1813        // Look up existing sketch.
1814        let sketch_id = sketch;
1815        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1816            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1817        })?;
1818        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1819            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1820                "Object is not a sketch, it is {}",
1821                sketch_object.kind.human_friendly_kind_with_article(),
1822            ))));
1823        };
1824        // Add the line to the AST of the sketch block.
1825        let mut new_ast = self.program.ast.clone();
1826        let (sketch_block_ref, _) = self
1827            .mutate_ast(
1828                &mut new_ast,
1829                sketch_id,
1830                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1831            )
1832            .map_err(KclErrorWithOutputs::no_outputs)?;
1833        // Convert to string source to create real source ranges.
1834        let new_source = source_from_ast(&new_ast);
1835        // Parse the new KCL source.
1836        let (new_program, errors) = Program::parse(&new_source)
1837            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1838        if !errors.is_empty() {
1839            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1840                "Error parsing KCL source after adding line: {errors:?}"
1841            ))));
1842        }
1843        let Some(new_program) = new_program else {
1844            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1845                "No AST produced after adding line".to_string(),
1846            )));
1847        };
1848
1849        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1850            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1851                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1852            )))
1853        })?;
1854        #[cfg(not(feature = "artifact-graph"))]
1855        let _ = line_node_ref;
1856
1857        // Make sure to only set this if there are no errors.
1858        self.program = new_program.clone();
1859
1860        // Truncate after the sketch block for mock execution.
1861        let mut truncated_program = new_program;
1862        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1863            .map_err(KclErrorWithOutputs::no_outputs)?;
1864
1865        // Execute.
1866        let outcome = ctx
1867            .run_mock(
1868                &truncated_program,
1869                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1870            )
1871            .await?;
1872
1873        #[cfg(not(feature = "artifact-graph"))]
1874        let new_object_ids = Vec::new();
1875        #[cfg(feature = "artifact-graph")]
1876        let new_object_ids = {
1877            let make_err =
1878                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1879            let segment_id = outcome
1880                .source_range_to_object
1881                .get(&line_node_ref.range)
1882                .copied()
1883                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1884            let segment_object = outcome
1885                .scene_object_by_id(segment_id)
1886                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1887            let ObjectKind::Segment { segment } = &segment_object.kind else {
1888                return Err(make_err(format!(
1889                    "Object is not a segment, it is {}",
1890                    segment_object.kind.human_friendly_kind_with_article()
1891                )));
1892            };
1893            let Segment::Line(line) = segment else {
1894                return Err(make_err(format!(
1895                    "Segment is not a line, it is {}",
1896                    segment.human_friendly_kind_with_article()
1897                )));
1898            };
1899            vec![line.start, line.end, segment_id]
1900        };
1901        let src_delta = SourceDelta { text: new_source };
1902        // Uses .no_freedom_analysis() so freedom_analysis: false
1903        let outcome = self.update_state_after_exec(outcome, false);
1904        let scene_graph_delta = SceneGraphDelta {
1905            new_graph: self.scene_graph.clone(),
1906            invalidates_ids: false,
1907            new_objects: new_object_ids,
1908            exec_outcome: outcome,
1909        };
1910        Ok((src_delta, scene_graph_delta))
1911    }
1912
1913    async fn add_arc(
1914        &mut self,
1915        ctx: &ExecutorContext,
1916        sketch: ObjectId,
1917        ctor: ArcCtor,
1918    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1919        // Create updated KCL source from args.
1920        let start_ast = to_ast_point2d(&ctor.start)
1921            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1922        let end_ast = to_ast_point2d(&ctor.end)
1923            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1924        let center_ast = to_ast_point2d(&ctor.center)
1925            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1926        let mut arguments = vec![
1927            ast::LabeledArg {
1928                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1929                arg: start_ast,
1930            },
1931            ast::LabeledArg {
1932                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1933                arg: end_ast,
1934            },
1935            ast::LabeledArg {
1936                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1937                arg: center_ast,
1938            },
1939        ];
1940        // Add construction kwarg if construction is Some(true)
1941        if ctor.construction == Some(true) {
1942            arguments.push(ast::LabeledArg {
1943                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1944                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1945                    value: ast::LiteralValue::Bool(true),
1946                    raw: "true".to_string(),
1947                    digest: None,
1948                }))),
1949            });
1950        }
1951        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1952            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1953            unlabeled: None,
1954            arguments,
1955            digest: None,
1956            non_code_meta: Default::default(),
1957        })));
1958
1959        // Look up existing sketch.
1960        let sketch_id = sketch;
1961        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1962            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1963        })?;
1964        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1965            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1966                "Object is not a sketch, it is {}",
1967                sketch_object.kind.human_friendly_kind_with_article(),
1968            ))));
1969        };
1970        // Add the arc to the AST of the sketch block.
1971        let mut new_ast = self.program.ast.clone();
1972        let (sketch_block_ref, _) = self
1973            .mutate_ast(
1974                &mut new_ast,
1975                sketch_id,
1976                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1977            )
1978            .map_err(KclErrorWithOutputs::no_outputs)?;
1979        // Convert to string source to create real source ranges.
1980        let new_source = source_from_ast(&new_ast);
1981        // Parse the new KCL source.
1982        let (new_program, errors) = Program::parse(&new_source)
1983            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1984        if !errors.is_empty() {
1985            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1986                "Error parsing KCL source after adding arc: {errors:?}"
1987            ))));
1988        }
1989        let Some(new_program) = new_program else {
1990            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1991                "No AST produced after adding arc".to_string(),
1992            )));
1993        };
1994
1995        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1996            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1997                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
1998            )))
1999        })?;
2000        #[cfg(not(feature = "artifact-graph"))]
2001        let _ = arc_node_ref;
2002
2003        // Make sure to only set this if there are no errors.
2004        self.program = new_program.clone();
2005
2006        // Truncate after the sketch block for mock execution.
2007        let mut truncated_program = new_program;
2008        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2009            .map_err(KclErrorWithOutputs::no_outputs)?;
2010
2011        // Execute.
2012        let outcome = ctx
2013            .run_mock(
2014                &truncated_program,
2015                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2016            )
2017            .await?;
2018
2019        #[cfg(not(feature = "artifact-graph"))]
2020        let new_object_ids = Vec::new();
2021        #[cfg(feature = "artifact-graph")]
2022        let new_object_ids = {
2023            let make_err =
2024                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2025            let segment_id = outcome
2026                .source_range_to_object
2027                .get(&arc_node_ref.range)
2028                .copied()
2029                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2030            let segment_object = outcome
2031                .scene_objects
2032                .get(segment_id.0)
2033                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2034            let ObjectKind::Segment { segment } = &segment_object.kind else {
2035                return Err(make_err(format!(
2036                    "Object is not a segment, it is {}",
2037                    segment_object.kind.human_friendly_kind_with_article()
2038                )));
2039            };
2040            let Segment::Arc(arc) = segment else {
2041                return Err(make_err(format!(
2042                    "Segment is not an arc, it is {}",
2043                    segment.human_friendly_kind_with_article()
2044                )));
2045            };
2046            vec![arc.start, arc.end, arc.center, segment_id]
2047        };
2048        let src_delta = SourceDelta { text: new_source };
2049        // Uses .no_freedom_analysis() so freedom_analysis: false
2050        let outcome = self.update_state_after_exec(outcome, false);
2051        let scene_graph_delta = SceneGraphDelta {
2052            new_graph: self.scene_graph.clone(),
2053            invalidates_ids: false,
2054            new_objects: new_object_ids,
2055            exec_outcome: outcome,
2056        };
2057        Ok((src_delta, scene_graph_delta))
2058    }
2059
2060    async fn add_circle(
2061        &mut self,
2062        ctx: &ExecutorContext,
2063        sketch: ObjectId,
2064        ctor: CircleCtor,
2065    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2066        // Create updated KCL source from args.
2067        let start_ast = to_ast_point2d(&ctor.start)
2068            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2069        let center_ast = to_ast_point2d(&ctor.center)
2070            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2071        let mut arguments = vec![
2072            ast::LabeledArg {
2073                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2074                arg: start_ast,
2075            },
2076            ast::LabeledArg {
2077                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2078                arg: center_ast,
2079            },
2080        ];
2081        // Add construction kwarg if construction is Some(true)
2082        if ctor.construction == Some(true) {
2083            arguments.push(ast::LabeledArg {
2084                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2085                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2086                    value: ast::LiteralValue::Bool(true),
2087                    raw: "true".to_string(),
2088                    digest: None,
2089                }))),
2090            });
2091        }
2092        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2093            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2094            unlabeled: None,
2095            arguments,
2096            digest: None,
2097            non_code_meta: Default::default(),
2098        })));
2099
2100        // Look up existing sketch.
2101        let sketch_id = sketch;
2102        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2104        })?;
2105        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2106            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2107                "Object is not a sketch, it is {}",
2108                sketch_object.kind.human_friendly_kind_with_article(),
2109            ))));
2110        };
2111        // Add the circle to the AST of the sketch block.
2112        let mut new_ast = self.program.ast.clone();
2113        let (sketch_block_ref, _) = self
2114            .mutate_ast(
2115                &mut new_ast,
2116                sketch_id,
2117                AstMutateCommand::AddSketchBlockVarDecl {
2118                    prefix: CIRCLE_VARIABLE.to_owned(),
2119                    expr: circle_ast,
2120                },
2121            )
2122            .map_err(KclErrorWithOutputs::no_outputs)?;
2123        // Convert to string source to create real source ranges.
2124        let new_source = source_from_ast(&new_ast);
2125        // Parse the new KCL source.
2126        let (new_program, errors) = Program::parse(&new_source)
2127            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2128        if !errors.is_empty() {
2129            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2130                "Error parsing KCL source after adding circle: {errors:?}"
2131            ))));
2132        }
2133        let Some(new_program) = new_program else {
2134            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2135                "No AST produced after adding circle".to_string(),
2136            )));
2137        };
2138
2139        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2140            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2141                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2142            )))
2143        })?;
2144        #[cfg(not(feature = "artifact-graph"))]
2145        let _ = circle_node_ref;
2146
2147        // Make sure to only set this if there are no errors.
2148        self.program = new_program.clone();
2149
2150        // Truncate after the sketch block for mock execution.
2151        let mut truncated_program = new_program;
2152        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2153            .map_err(KclErrorWithOutputs::no_outputs)?;
2154
2155        // Execute.
2156        let outcome = ctx
2157            .run_mock(
2158                &truncated_program,
2159                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2160            )
2161            .await?;
2162
2163        #[cfg(not(feature = "artifact-graph"))]
2164        let new_object_ids = Vec::new();
2165        #[cfg(feature = "artifact-graph")]
2166        let new_object_ids = {
2167            let make_err =
2168                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2169            let segment_id = outcome
2170                .source_range_to_object
2171                .get(&circle_node_ref.range)
2172                .copied()
2173                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2174            let segment_object = outcome
2175                .scene_objects
2176                .get(segment_id.0)
2177                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2178            let ObjectKind::Segment { segment } = &segment_object.kind else {
2179                return Err(make_err(format!(
2180                    "Object is not a segment, it is {}",
2181                    segment_object.kind.human_friendly_kind_with_article()
2182                )));
2183            };
2184            let Segment::Circle(circle) = segment else {
2185                return Err(make_err(format!(
2186                    "Segment is not a circle, it is {}",
2187                    segment.human_friendly_kind_with_article()
2188                )));
2189            };
2190            vec![circle.start, circle.center, segment_id]
2191        };
2192        let src_delta = SourceDelta { text: new_source };
2193        // Uses .no_freedom_analysis() so freedom_analysis: false
2194        let outcome = self.update_state_after_exec(outcome, false);
2195        let scene_graph_delta = SceneGraphDelta {
2196            new_graph: self.scene_graph.clone(),
2197            invalidates_ids: false,
2198            new_objects: new_object_ids,
2199            exec_outcome: outcome,
2200        };
2201        Ok((src_delta, scene_graph_delta))
2202    }
2203
2204    fn edit_point(
2205        &mut self,
2206        new_ast: &mut ast::Node<ast::Program>,
2207        sketch: ObjectId,
2208        point: ObjectId,
2209        ctor: PointCtor,
2210    ) -> Result<(), KclError> {
2211        // Create updated KCL source from args.
2212        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2213
2214        // Look up existing sketch.
2215        let sketch_id = sketch;
2216        let sketch_object = self
2217            .scene_graph
2218            .objects
2219            .get(sketch_id.0)
2220            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2221        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2222            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2223        };
2224        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2225            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2226        })?;
2227        // Look up existing point.
2228        let point_id = point;
2229        let point_object = self
2230            .scene_graph
2231            .objects
2232            .get(point_id.0)
2233            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2234        let ObjectKind::Segment {
2235            segment: Segment::Point(point),
2236        } = &point_object.kind
2237        else {
2238            return Err(KclError::refactor(format!(
2239                "Object is not a point segment: {point_object:?}"
2240            )));
2241        };
2242
2243        // If the point is part of a line or arc, edit the line/arc instead.
2244        if let Some(owner_id) = point.owner {
2245            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2246                KclError::refactor(format!(
2247                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2248                ))
2249            })?;
2250            let ObjectKind::Segment { segment } = &owner_object.kind else {
2251                return Err(KclError::refactor(format!(
2252                    "Internal: Owner of point is not a segment, but found {}",
2253                    owner_object.kind.human_friendly_kind_with_article()
2254                )));
2255            };
2256
2257            // Handle Line owner
2258            if let Segment::Line(line) = segment {
2259                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2260                    return Err(KclError::refactor(format!(
2261                        "Internal: Owner of point does not have line ctor, but found {}",
2262                        line.ctor.human_friendly_kind_with_article()
2263                    )));
2264                };
2265                let mut line_ctor = line_ctor.clone();
2266                // Which end of the line is this point?
2267                if line.start == point_id {
2268                    line_ctor.start = ctor.position;
2269                } else if line.end == point_id {
2270                    line_ctor.end = ctor.position;
2271                } else {
2272                    return Err(KclError::refactor(format!(
2273                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2274                    )));
2275                }
2276                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2277            }
2278
2279            // Handle Arc owner
2280            if let Segment::Arc(arc) = segment {
2281                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2282                    return Err(KclError::refactor(format!(
2283                        "Internal: Owner of point does not have arc ctor, but found {}",
2284                        arc.ctor.human_friendly_kind_with_article()
2285                    )));
2286                };
2287                let mut arc_ctor = arc_ctor.clone();
2288                // Which point of the arc is this? (center, start, or end)
2289                if arc.center == point_id {
2290                    arc_ctor.center = ctor.position;
2291                } else if arc.start == point_id {
2292                    arc_ctor.start = ctor.position;
2293                } else if arc.end == point_id {
2294                    arc_ctor.end = ctor.position;
2295                } else {
2296                    return Err(KclError::refactor(format!(
2297                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2298                    )));
2299                }
2300                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2301            }
2302
2303            // Handle Circle owner
2304            if let Segment::Circle(circle) = segment {
2305                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2306                    return Err(KclError::refactor(format!(
2307                        "Internal: Owner of point does not have circle ctor, but found {}",
2308                        circle.ctor.human_friendly_kind_with_article()
2309                    )));
2310                };
2311                let mut circle_ctor = circle_ctor.clone();
2312                if circle.center == point_id {
2313                    circle_ctor.center = ctor.position;
2314                } else if circle.start == point_id {
2315                    circle_ctor.start = ctor.position;
2316                } else {
2317                    return Err(KclError::refactor(format!(
2318                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2319                    )));
2320                }
2321                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2322            }
2323
2324            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2325            // (fall through to the point editing logic below)
2326        }
2327
2328        // Modify the point AST.
2329        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2330        Ok(())
2331    }
2332
2333    fn edit_line(
2334        &mut self,
2335        new_ast: &mut ast::Node<ast::Program>,
2336        sketch: ObjectId,
2337        line: ObjectId,
2338        ctor: LineCtor,
2339    ) -> Result<(), KclError> {
2340        // Create updated KCL source from args.
2341        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2342        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2343
2344        // Look up existing sketch.
2345        let sketch_id = sketch;
2346        let sketch_object = self
2347            .scene_graph
2348            .objects
2349            .get(sketch_id.0)
2350            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2351        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2352            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2353        };
2354        sketch
2355            .segments
2356            .iter()
2357            .find(|o| **o == line)
2358            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2359        // Look up existing line.
2360        let line_id = line;
2361        let line_object = self
2362            .scene_graph
2363            .objects
2364            .get(line_id.0)
2365            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2366        let ObjectKind::Segment { .. } = &line_object.kind else {
2367            let kind = line_object.kind.human_friendly_kind_with_article();
2368            return Err(KclError::refactor(format!(
2369                "This constraint only works on Segments, but you selected {kind}"
2370            )));
2371        };
2372
2373        // Modify the line AST.
2374        self.mutate_ast(
2375            new_ast,
2376            line_id,
2377            AstMutateCommand::EditLine {
2378                start: new_start_ast,
2379                end: new_end_ast,
2380                construction: ctor.construction,
2381            },
2382        )?;
2383        Ok(())
2384    }
2385
2386    fn edit_arc(
2387        &mut self,
2388        new_ast: &mut ast::Node<ast::Program>,
2389        sketch: ObjectId,
2390        arc: ObjectId,
2391        ctor: ArcCtor,
2392    ) -> Result<(), KclError> {
2393        // Create updated KCL source from args.
2394        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2395        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2396        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2397
2398        // Look up existing sketch.
2399        let sketch_id = sketch;
2400        let sketch_object = self
2401            .scene_graph
2402            .objects
2403            .get(sketch_id.0)
2404            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2405        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2406            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2407        };
2408        sketch
2409            .segments
2410            .iter()
2411            .find(|o| **o == arc)
2412            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2413        // Look up existing arc.
2414        let arc_id = arc;
2415        let arc_object = self
2416            .scene_graph
2417            .objects
2418            .get(arc_id.0)
2419            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2420        let ObjectKind::Segment { .. } = &arc_object.kind else {
2421            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2422        };
2423
2424        // Modify the arc AST.
2425        self.mutate_ast(
2426            new_ast,
2427            arc_id,
2428            AstMutateCommand::EditArc {
2429                start: new_start_ast,
2430                end: new_end_ast,
2431                center: new_center_ast,
2432                construction: ctor.construction,
2433            },
2434        )?;
2435        Ok(())
2436    }
2437
2438    fn edit_circle(
2439        &mut self,
2440        new_ast: &mut ast::Node<ast::Program>,
2441        sketch: ObjectId,
2442        circle: ObjectId,
2443        ctor: CircleCtor,
2444    ) -> Result<(), KclError> {
2445        // Create updated KCL source from args.
2446        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2447        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2448
2449        // Look up existing sketch.
2450        let sketch_id = sketch;
2451        let sketch_object = self
2452            .scene_graph
2453            .objects
2454            .get(sketch_id.0)
2455            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2456        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2457            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2458        };
2459        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2460            KclError::refactor(format!(
2461                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2462            ))
2463        })?;
2464        // Look up existing circle.
2465        let circle_id = circle;
2466        let circle_object = self
2467            .scene_graph
2468            .objects
2469            .get(circle_id.0)
2470            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2471        let ObjectKind::Segment { .. } = &circle_object.kind else {
2472            return Err(KclError::refactor(format!(
2473                "Object is not a segment: {circle_object:?}"
2474            )));
2475        };
2476
2477        // Modify the circle AST.
2478        self.mutate_ast(
2479            new_ast,
2480            circle_id,
2481            AstMutateCommand::EditCircle {
2482                start: new_start_ast,
2483                center: new_center_ast,
2484                construction: ctor.construction,
2485            },
2486        )?;
2487        Ok(())
2488    }
2489
2490    fn delete_segment(
2491        &mut self,
2492        new_ast: &mut ast::Node<ast::Program>,
2493        sketch: ObjectId,
2494        segment_id: ObjectId,
2495    ) -> Result<(), KclError> {
2496        // Look up existing sketch.
2497        let sketch_id = sketch;
2498        let sketch_object = self
2499            .scene_graph
2500            .objects
2501            .get(sketch_id.0)
2502            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2503        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2504            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2505        };
2506        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2507            KclError::refactor(format!(
2508                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2509            ))
2510        })?;
2511        // Look up existing segment.
2512        let segment_object =
2513            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2514                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2515            })?;
2516        let ObjectKind::Segment { .. } = &segment_object.kind else {
2517            return Err(KclError::refactor(format!(
2518                "Object is not a segment, it is {}",
2519                segment_object.kind.human_friendly_kind_with_article()
2520            )));
2521        };
2522
2523        // Modify the AST to remove the segment.
2524        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2525        Ok(())
2526    }
2527
2528    fn delete_constraint(
2529        &mut self,
2530        new_ast: &mut ast::Node<ast::Program>,
2531        sketch: ObjectId,
2532        constraint_id: ObjectId,
2533    ) -> Result<(), KclError> {
2534        // Look up existing sketch.
2535        let sketch_id = sketch;
2536        let sketch_object = self
2537            .scene_graph
2538            .objects
2539            .get(sketch_id.0)
2540            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2541        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2542            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2543        };
2544        sketch
2545            .constraints
2546            .iter()
2547            .find(|o| **o == constraint_id)
2548            .ok_or_else(|| {
2549                KclError::refactor(format!(
2550                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2551                ))
2552            })?;
2553        // Look up existing constraint.
2554        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2555            KclError::refactor(format!(
2556                "Constraint not found in scene graph: constraint={constraint_id:?}"
2557            ))
2558        })?;
2559        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2560            return Err(KclError::refactor(format!(
2561                "Object is not a constraint, it is {}",
2562                constraint_object.kind.human_friendly_kind_with_article()
2563            )));
2564        };
2565
2566        // Modify the AST to remove the constraint.
2567        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2568        Ok(())
2569    }
2570
2571    fn edit_coincident_constraint(
2572        &mut self,
2573        new_ast: &mut ast::Node<ast::Program>,
2574        constraint_id: ObjectId,
2575        segments: Vec<ConstraintSegment>,
2576    ) -> Result<(), KclError> {
2577        if segments.len() < 2 {
2578            return Err(KclError::refactor(format!(
2579                "Coincident constraint must have at least 2 inputs, got {}",
2580                segments.len()
2581            )));
2582        }
2583
2584        let segment_asts = segments
2585            .iter()
2586            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2587            .collect::<Result<Vec<_>, _>>()?;
2588
2589        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2590            elements: segment_asts,
2591            digest: None,
2592            non_code_meta: Default::default(),
2593        })));
2594
2595        self.mutate_ast(
2596            new_ast,
2597            constraint_id,
2598            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2599        )?;
2600        Ok(())
2601    }
2602
2603    fn edit_horizontal_points_constraint(
2604        &mut self,
2605        new_ast: &mut ast::Node<ast::Program>,
2606        constraint_id: ObjectId,
2607        points: Vec<ConstraintSegment>,
2608    ) -> Result<(), KclError> {
2609        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2610    }
2611
2612    fn edit_vertical_points_constraint(
2613        &mut self,
2614        new_ast: &mut ast::Node<ast::Program>,
2615        constraint_id: ObjectId,
2616        points: Vec<ConstraintSegment>,
2617    ) -> Result<(), KclError> {
2618        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2619    }
2620
2621    fn edit_axis_points_constraint(
2622        &mut self,
2623        new_ast: &mut ast::Node<ast::Program>,
2624        constraint_id: ObjectId,
2625        points: Vec<ConstraintSegment>,
2626        constraint_name: &str,
2627    ) -> Result<(), KclError> {
2628        if points.len() < 2 {
2629            return Err(KclError::refactor(format!(
2630                "{constraint_name} points constraint must have at least 2 points, got {}",
2631                points.len()
2632            )));
2633        }
2634
2635        let point_asts = points
2636            .iter()
2637            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2638            .collect::<Result<Vec<_>, _>>()?;
2639
2640        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2641            elements: point_asts,
2642            digest: None,
2643            non_code_meta: Default::default(),
2644        })));
2645
2646        self.mutate_ast(
2647            new_ast,
2648            constraint_id,
2649            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2650        )?;
2651        Ok(())
2652    }
2653
2654    /// updates the equalLength constraint with the given lines
2655    fn edit_equal_length_constraint(
2656        &mut self,
2657        new_ast: &mut ast::Node<ast::Program>,
2658        constraint_id: ObjectId,
2659        lines: Vec<ObjectId>,
2660    ) -> Result<(), KclError> {
2661        if lines.len() < 2 {
2662            return Err(KclError::refactor(format!(
2663                "Lines equal length constraint must have at least 2 lines, got {}",
2664                lines.len()
2665            )));
2666        }
2667
2668        let line_asts = lines
2669            .iter()
2670            .map(|line_id| {
2671                let line_object = self
2672                    .scene_graph
2673                    .objects
2674                    .get(line_id.0)
2675                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2676                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2677                    let kind = line_object.kind.human_friendly_kind_with_article();
2678                    return Err(KclError::refactor(format!(
2679                        "This constraint only works on Segments, but you selected {kind}"
2680                    )));
2681                };
2682                let Segment::Line(_) = line_segment else {
2683                    let kind = line_segment.human_friendly_kind_with_article();
2684                    return Err(KclError::refactor(format!(
2685                        "Only lines can be made equal length, but you selected {kind}"
2686                    )));
2687                };
2688
2689                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2690            })
2691            .collect::<Result<Vec<_>, _>>()?;
2692
2693        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2694            elements: line_asts,
2695            digest: None,
2696            non_code_meta: Default::default(),
2697        })));
2698
2699        self.mutate_ast(
2700            new_ast,
2701            constraint_id,
2702            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2703        )?;
2704        Ok(())
2705    }
2706
2707    /// Updates the parallel constraint with the given lines.
2708    fn edit_parallel_constraint(
2709        &mut self,
2710        new_ast: &mut ast::Node<ast::Program>,
2711        constraint_id: ObjectId,
2712        lines: Vec<ObjectId>,
2713    ) -> Result<(), KclError> {
2714        if lines.len() < 2 {
2715            return Err(KclError::refactor(format!(
2716                "Parallel constraint must have at least 2 lines, got {}",
2717                lines.len()
2718            )));
2719        }
2720
2721        let line_asts = lines
2722            .iter()
2723            .map(|line_id| {
2724                let line_object = self
2725                    .scene_graph
2726                    .objects
2727                    .get(line_id.0)
2728                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2729                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2730                    let kind = line_object.kind.human_friendly_kind_with_article();
2731                    return Err(KclError::refactor(format!(
2732                        "This constraint only works on Segments, but you selected {kind}"
2733                    )));
2734                };
2735                let Segment::Line(_) = line_segment else {
2736                    let kind = line_segment.human_friendly_kind_with_article();
2737                    return Err(KclError::refactor(format!(
2738                        "Only lines can be made parallel, but you selected {kind}"
2739                    )));
2740                };
2741
2742                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2743            })
2744            .collect::<Result<Vec<_>, _>>()?;
2745
2746        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2747            elements: line_asts,
2748            digest: None,
2749            non_code_meta: Default::default(),
2750        })));
2751
2752        self.mutate_ast(
2753            new_ast,
2754            constraint_id,
2755            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2756        )?;
2757        Ok(())
2758    }
2759
2760    /// Updates the equalRadius constraint with the given segments.
2761    fn edit_equal_radius_constraint(
2762        &mut self,
2763        new_ast: &mut ast::Node<ast::Program>,
2764        constraint_id: ObjectId,
2765        input: Vec<ObjectId>,
2766    ) -> Result<(), KclError> {
2767        if input.len() < 2 {
2768            return Err(KclError::refactor(format!(
2769                "equalRadius constraint must have at least 2 segments, got {}",
2770                input.len()
2771            )));
2772        }
2773
2774        let input_asts = input
2775            .iter()
2776            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2777            .collect::<Result<Vec<_>, _>>()?;
2778
2779        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2780            elements: input_asts,
2781            digest: None,
2782            non_code_meta: Default::default(),
2783        })));
2784
2785        self.mutate_ast(
2786            new_ast,
2787            constraint_id,
2788            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2789        )?;
2790        Ok(())
2791    }
2792
2793    async fn execute_after_edit(
2794        &mut self,
2795        ctx: &ExecutorContext,
2796        sketch: ObjectId,
2797        sketch_block_ref: AstNodeRef,
2798        segment_ids_edited: AhashIndexSet<ObjectId>,
2799        edit_kind: EditDeleteKind,
2800        new_ast: &mut ast::Node<ast::Program>,
2801    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2802        // Convert to string source to create real source ranges.
2803        let new_source = source_from_ast(new_ast);
2804        // Parse the new KCL source.
2805        let (new_program, errors) = Program::parse(&new_source)
2806            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2807        if !errors.is_empty() {
2808            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2809                "Error parsing KCL source after editing: {errors:?}"
2810            ))));
2811        }
2812        let Some(new_program) = new_program else {
2813            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2814                "No AST produced after editing".to_string(),
2815            )));
2816        };
2817
2818        // TODO: sketch-api: make sure to only set this if there are no errors.
2819        self.program = new_program.clone();
2820
2821        // Truncate after the sketch block for mock execution.
2822        let is_delete = edit_kind.is_delete();
2823        let truncated_program = {
2824            let mut truncated_program = new_program;
2825            only_sketch_block(
2826                &mut truncated_program.ast,
2827                &sketch_block_ref,
2828                edit_kind.to_change_kind(),
2829            )
2830            .map_err(KclErrorWithOutputs::no_outputs)?;
2831            truncated_program
2832        };
2833
2834        #[cfg(not(feature = "artifact-graph"))]
2835        drop(segment_ids_edited);
2836
2837        // Execute.
2838        let mock_config = MockConfig {
2839            sketch_block_id: Some(sketch),
2840            freedom_analysis: is_delete,
2841            #[cfg(feature = "artifact-graph")]
2842            segment_ids_edited: segment_ids_edited.clone(),
2843            ..Default::default()
2844        };
2845        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2846
2847        // Uses freedom_analysis: is_delete
2848        let outcome = self.update_state_after_exec(outcome, is_delete);
2849
2850        #[cfg(feature = "artifact-graph")]
2851        let new_source = {
2852            // Feed back sketch var solutions into the source.
2853            //
2854            // The interpreter is returning all var solutions from the sketch
2855            // block we're editing.
2856            let mut new_ast = self.program.ast.clone();
2857            for (var_range, value) in &outcome.var_solutions {
2858                let rounded = value.round(3);
2859                let source_ref = SourceRef::Simple {
2860                    range: *var_range,
2861                    node_path: None,
2862                };
2863                mutate_ast_node_by_source_ref(
2864                    &mut new_ast,
2865                    &source_ref,
2866                    AstMutateCommand::EditVarInitialValue { value: rounded },
2867                )
2868                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2869            }
2870            source_from_ast(&new_ast)
2871        };
2872
2873        let src_delta = SourceDelta { text: new_source };
2874        let scene_graph_delta = SceneGraphDelta {
2875            new_graph: self.scene_graph.clone(),
2876            invalidates_ids: is_delete,
2877            new_objects: Vec::new(),
2878            exec_outcome: outcome,
2879        };
2880        Ok((src_delta, scene_graph_delta))
2881    }
2882
2883    async fn execute_after_delete_sketch(
2884        &mut self,
2885        ctx: &ExecutorContext,
2886        new_ast: &mut ast::Node<ast::Program>,
2887    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2888        // Convert to string source to create real source ranges.
2889        let new_source = source_from_ast(new_ast);
2890        // Parse the new KCL source.
2891        let (new_program, errors) = Program::parse(&new_source)
2892            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2893        if !errors.is_empty() {
2894            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2895                "Error parsing KCL source after editing: {errors:?}"
2896            ))));
2897        }
2898        let Some(new_program) = new_program else {
2899            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2900                "No AST produced after editing".to_string(),
2901            )));
2902        };
2903
2904        // Make sure to only set this if there are no errors.
2905        self.program = new_program.clone();
2906
2907        // We deleted the entire sketch block. It doesn't make sense to truncate
2908        // and execute only the sketch block. We execute the whole program with
2909        // a real engine.
2910
2911        // Execute.
2912        let outcome = ctx.run_with_caching(new_program).await?;
2913        let freedom_analysis_ran = true;
2914
2915        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2916
2917        let src_delta = SourceDelta { text: new_source };
2918        let scene_graph_delta = SceneGraphDelta {
2919            new_graph: self.scene_graph.clone(),
2920            invalidates_ids: true,
2921            new_objects: Vec::new(),
2922            exec_outcome: outcome,
2923        };
2924        Ok((src_delta, scene_graph_delta))
2925    }
2926
2927    /// Map a point object id into an AST reference expression for use in
2928    /// constraints. If the point is owned by a segment (line or arc), we
2929    /// reference the appropriate property on that segment (e.g. `line1.start`,
2930    /// `arc1.center`). Otherwise we reference the point directly.
2931    fn point_id_to_ast_reference(
2932        &self,
2933        point_id: ObjectId,
2934        new_ast: &mut ast::Node<ast::Program>,
2935    ) -> Result<ast::Expr, KclError> {
2936        let point_object = self
2937            .scene_graph
2938            .objects
2939            .get(point_id.0)
2940            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2941        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2942            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2943        };
2944        let Segment::Point(point) = point_segment else {
2945            return Err(KclError::refactor(format!(
2946                "Only points are currently supported: {point_object:?}"
2947            )));
2948        };
2949
2950        if let Some(owner_id) = point.owner {
2951            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2952                KclError::refactor(format!(
2953                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2954                ))
2955            })?;
2956            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2957                return Err(KclError::refactor(format!(
2958                    "Owner of point is not a segment, but found {}",
2959                    owner_object.kind.human_friendly_kind_with_article()
2960                )));
2961            };
2962
2963            match owner_segment {
2964                Segment::Line(line) => {
2965                    let property = if line.start == point_id {
2966                        LINE_PROPERTY_START
2967                    } else if line.end == point_id {
2968                        LINE_PROPERTY_END
2969                    } else {
2970                        return Err(KclError::refactor(format!(
2971                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2972                        )));
2973                    };
2974                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2975                }
2976                Segment::Arc(arc) => {
2977                    let property = if arc.start == point_id {
2978                        ARC_PROPERTY_START
2979                    } else if arc.end == point_id {
2980                        ARC_PROPERTY_END
2981                    } else if arc.center == point_id {
2982                        ARC_PROPERTY_CENTER
2983                    } else {
2984                        return Err(KclError::refactor(format!(
2985                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2986                        )));
2987                    };
2988                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2989                }
2990                Segment::Circle(circle) => {
2991                    let property = if circle.start == point_id {
2992                        CIRCLE_PROPERTY_START
2993                    } else if circle.center == point_id {
2994                        CIRCLE_PROPERTY_CENTER
2995                    } else {
2996                        return Err(KclError::refactor(format!(
2997                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2998                        )));
2999                    };
3000                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3001                }
3002                _ => Err(KclError::refactor(format!(
3003                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3004                ))),
3005            }
3006        } else {
3007            // Standalone point.
3008            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3009        }
3010    }
3011
3012    fn coincident_segment_to_ast(
3013        &self,
3014        segment: &ConstraintSegment,
3015        new_ast: &mut ast::Node<ast::Program>,
3016    ) -> Result<ast::Expr, KclError> {
3017        match segment {
3018            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3019            ConstraintSegment::Segment(segment_id) => {
3020                let segment_object = self
3021                    .scene_graph
3022                    .objects
3023                    .get(segment_id.0)
3024                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3025                let ObjectKind::Segment { segment } = &segment_object.kind else {
3026                    return Err(KclError::refactor(format!(
3027                        "Object is not a segment, it is {}",
3028                        segment_object.kind.human_friendly_kind_with_article()
3029                    )));
3030                };
3031
3032                match segment {
3033                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3034                    Segment::Line(_) => {
3035                        get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3036                    }
3037                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3038                    Segment::Circle(_) => {
3039                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3040                    }
3041                }
3042            }
3043        }
3044    }
3045
3046    fn axis_constraint_segment_to_ast(
3047        &self,
3048        segment: &ConstraintSegment,
3049        new_ast: &mut ast::Node<ast::Program>,
3050    ) -> Result<ast::Expr, KclError> {
3051        match segment {
3052            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3053            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3054        }
3055    }
3056
3057    async fn add_coincident(
3058        &mut self,
3059        sketch: ObjectId,
3060        coincident: Coincident,
3061        new_ast: &mut ast::Node<ast::Program>,
3062    ) -> Result<AstNodeRef, KclError> {
3063        let sketch_id = sketch;
3064        let segment_asts = coincident
3065            .segments
3066            .iter()
3067            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3068            .collect::<Result<Vec<_>, _>>()?;
3069        if segment_asts.len() < 2 {
3070            return Err(KclError::refactor(format!(
3071                "Coincident constraint must have at least 2 inputs, got {}",
3072                segment_asts.len()
3073            )));
3074        }
3075
3076        // Create the coincident() call using shared helper.
3077        let coincident_ast = create_coincident_ast(segment_asts);
3078
3079        // Add the line to the AST of the sketch block.
3080        let (sketch_block_ref, _) = self.mutate_ast(
3081            new_ast,
3082            sketch_id,
3083            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3084        )?;
3085        Ok(sketch_block_ref)
3086    }
3087
3088    async fn add_distance(
3089        &mut self,
3090        sketch: ObjectId,
3091        distance: Distance,
3092        new_ast: &mut ast::Node<ast::Program>,
3093    ) -> Result<AstNodeRef, KclError> {
3094        let sketch_id = sketch;
3095        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3096            [pt0, pt1] => [
3097                self.coincident_segment_to_ast(pt0, new_ast)?,
3098                self.coincident_segment_to_ast(pt1, new_ast)?,
3099            ],
3100            _ => {
3101                return Err(KclError::refactor(format!(
3102                    "Distance constraint must have exactly 2 points, got {}",
3103                    distance.points.len()
3104                )));
3105            }
3106        };
3107
3108        let arguments = match &distance.label_position {
3109            Some(label_position) => vec![ast::LabeledArg {
3110                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3111                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3112            }],
3113            None => Default::default(),
3114        };
3115
3116        // Create the distance() call.
3117        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3118            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3119            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3120                ast::ArrayExpression {
3121                    elements: vec![pt0_ast, pt1_ast],
3122                    digest: None,
3123                    non_code_meta: Default::default(),
3124                },
3125            )))),
3126            arguments,
3127            digest: None,
3128            non_code_meta: Default::default(),
3129        })));
3130        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3131            left: distance_call_ast,
3132            operator: ast::BinaryOperator::Eq,
3133            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3134                value: ast::LiteralValue::Number {
3135                    value: distance.distance.value,
3136                    suffix: distance.distance.units,
3137                },
3138                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3139                    KclError::refactor(format!(
3140                        "Could not format numeric suffix: {:?}",
3141                        distance.distance.units
3142                    ))
3143                })?,
3144                digest: None,
3145            }))),
3146            digest: None,
3147        })));
3148
3149        // Add the line to the AST of the sketch block.
3150        let (sketch_block_ref, _) = self.mutate_ast(
3151            new_ast,
3152            sketch_id,
3153            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3154        )?;
3155        Ok(sketch_block_ref)
3156    }
3157
3158    async fn add_angle(
3159        &mut self,
3160        sketch: ObjectId,
3161        angle: Angle,
3162        new_ast: &mut ast::Node<ast::Program>,
3163    ) -> Result<AstNodeRef, KclError> {
3164        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3165            return Err(KclError::refactor(format!(
3166                "Angle constraint must have exactly 2 lines, got {}",
3167                angle.lines.len()
3168            )));
3169        };
3170        let sketch_id = sketch;
3171
3172        // Map the runtime objects back to variable names.
3173        let line0_object = self
3174            .scene_graph
3175            .objects
3176            .get(l0_id.0)
3177            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3178        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3179            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3180        };
3181        let Segment::Line(_) = line0_segment else {
3182            return Err(KclError::refactor(format!(
3183                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3184            )));
3185        };
3186        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3187
3188        let line1_object = self
3189            .scene_graph
3190            .objects
3191            .get(l1_id.0)
3192            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3193        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3194            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3195        };
3196        let Segment::Line(_) = line1_segment else {
3197            return Err(KclError::refactor(format!(
3198                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3199            )));
3200        };
3201        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3202
3203        // Create the angle() call.
3204        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3205            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3206            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3207                ast::ArrayExpression {
3208                    elements: vec![l0_ast, l1_ast],
3209                    digest: None,
3210                    non_code_meta: Default::default(),
3211                },
3212            )))),
3213            arguments: Default::default(),
3214            digest: None,
3215            non_code_meta: Default::default(),
3216        })));
3217        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3218            left: angle_call_ast,
3219            operator: ast::BinaryOperator::Eq,
3220            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3221                value: ast::LiteralValue::Number {
3222                    value: angle.angle.value,
3223                    suffix: angle.angle.units,
3224                },
3225                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3226                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3227                })?,
3228                digest: None,
3229            }))),
3230            digest: None,
3231        })));
3232
3233        // Add the line to the AST of the sketch block.
3234        let (sketch_block_ref, _) = self.mutate_ast(
3235            new_ast,
3236            sketch_id,
3237            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3238        )?;
3239        Ok(sketch_block_ref)
3240    }
3241
3242    async fn add_tangent(
3243        &mut self,
3244        sketch: ObjectId,
3245        tangent: Tangent,
3246        new_ast: &mut ast::Node<ast::Program>,
3247    ) -> Result<AstNodeRef, KclError> {
3248        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3249            return Err(KclError::refactor(format!(
3250                "Tangent constraint must have exactly 2 segments, got {}",
3251                tangent.input.len()
3252            )));
3253        };
3254        let sketch_id = sketch;
3255
3256        let seg0_object = self
3257            .scene_graph
3258            .objects
3259            .get(seg0_id.0)
3260            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3261        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3262            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3263        };
3264        let seg0_ast = match seg0_segment {
3265            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3266            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3267            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3268            _ => {
3269                return Err(KclError::refactor(format!(
3270                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3271                )));
3272            }
3273        };
3274
3275        let seg1_object = self
3276            .scene_graph
3277            .objects
3278            .get(seg1_id.0)
3279            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3280        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3281            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3282        };
3283        let seg1_ast = match seg1_segment {
3284            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3285            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3286            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3287            _ => {
3288                return Err(KclError::refactor(format!(
3289                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3290                )));
3291            }
3292        };
3293
3294        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3295        let (sketch_block_ref, _) = self.mutate_ast(
3296            new_ast,
3297            sketch_id,
3298            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3299        )?;
3300        Ok(sketch_block_ref)
3301    }
3302
3303    async fn add_symmetric(
3304        &mut self,
3305        sketch: ObjectId,
3306        symmetric: Symmetric,
3307        new_ast: &mut ast::Node<ast::Program>,
3308    ) -> Result<AstNodeRef, KclError> {
3309        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3310            return Err(KclError::refactor(format!(
3311                "Symmetric constraint must have exactly 2 inputs, got {}",
3312                symmetric.input.len()
3313            )));
3314        };
3315        let sketch_id = sketch;
3316
3317        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3318        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3319        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3320
3321        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3322        let (sketch_block_ref, _) = self.mutate_ast(
3323            new_ast,
3324            sketch_id,
3325            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3326        )?;
3327        Ok(sketch_block_ref)
3328    }
3329
3330    async fn add_midpoint(
3331        &mut self,
3332        sketch: ObjectId,
3333        midpoint: Midpoint,
3334        new_ast: &mut ast::Node<ast::Program>,
3335    ) -> Result<AstNodeRef, KclError> {
3336        let sketch_id = sketch;
3337        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3338
3339        let segment_object = self
3340            .scene_graph
3341            .objects
3342            .get(midpoint.segment.0)
3343            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3344        let ObjectKind::Segment {
3345            segment: midpoint_segment,
3346        } = &segment_object.kind
3347        else {
3348            return Err(KclError::refactor(format!(
3349                "Object must be a segment, but it was {}",
3350                segment_object.kind.human_friendly_kind_with_article()
3351            )));
3352        };
3353        let segment_ast = match midpoint_segment {
3354            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3355            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3356            _ => {
3357                return Err(KclError::refactor(format!(
3358                    "Midpoint target must be a line or arc segment but it was {}",
3359                    midpoint_segment.human_friendly_kind_with_article()
3360                )));
3361            }
3362        };
3363
3364        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3365        let (sketch_block_ref, _) = self.mutate_ast(
3366            new_ast,
3367            sketch_id,
3368            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3369        )?;
3370        Ok(sketch_block_ref)
3371    }
3372
3373    async fn add_equal_radius(
3374        &mut self,
3375        sketch: ObjectId,
3376        equal_radius: EqualRadius,
3377        new_ast: &mut ast::Node<ast::Program>,
3378    ) -> Result<AstNodeRef, KclError> {
3379        if equal_radius.input.len() < 2 {
3380            return Err(KclError::refactor(format!(
3381                "equalRadius constraint must have at least 2 segments, got {}",
3382                equal_radius.input.len()
3383            )));
3384        }
3385
3386        let sketch_id = sketch;
3387        let input_asts = equal_radius
3388            .input
3389            .iter()
3390            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3391            .collect::<Result<Vec<_>, _>>()?;
3392
3393        let equal_radius_ast = create_equal_radius_ast(input_asts);
3394        let (sketch_block_ref, _) = self.mutate_ast(
3395            new_ast,
3396            sketch_id,
3397            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3398        )?;
3399        Ok(sketch_block_ref)
3400    }
3401
3402    async fn add_radius(
3403        &mut self,
3404        sketch: ObjectId,
3405        radius: Radius,
3406        new_ast: &mut ast::Node<ast::Program>,
3407    ) -> Result<AstNodeRef, KclError> {
3408        let params = ArcSizeConstraintParams {
3409            points: vec![radius.arc],
3410            function_name: RADIUS_FN,
3411            value: radius.radius.value,
3412            units: radius.radius.units,
3413            label_position: radius.label_position,
3414            constraint_type_name: "Radius",
3415        };
3416        self.add_arc_size_constraint(sketch, params, new_ast).await
3417    }
3418
3419    async fn add_diameter(
3420        &mut self,
3421        sketch: ObjectId,
3422        diameter: Diameter,
3423        new_ast: &mut ast::Node<ast::Program>,
3424    ) -> Result<AstNodeRef, KclError> {
3425        let params = ArcSizeConstraintParams {
3426            points: vec![diameter.arc],
3427            function_name: DIAMETER_FN,
3428            value: diameter.diameter.value,
3429            units: diameter.diameter.units,
3430            label_position: diameter.label_position,
3431            constraint_type_name: "Diameter",
3432        };
3433        self.add_arc_size_constraint(sketch, params, new_ast).await
3434    }
3435
3436    async fn add_fixed_constraints(
3437        &mut self,
3438        sketch: ObjectId,
3439        points: Vec<FixedPoint>,
3440        new_ast: &mut ast::Node<ast::Program>,
3441    ) -> Result<AstNodeRef, KclError> {
3442        let mut sketch_block_ref = None;
3443
3444        for fixed_point in points {
3445            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3446            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3447                .map_err(|err| KclError::refactor(err.to_string()))?;
3448
3449            let (sketch_ref, _) = self.mutate_ast(
3450                new_ast,
3451                sketch,
3452                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3453            )?;
3454            sketch_block_ref = Some(sketch_ref);
3455        }
3456
3457        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3458    }
3459
3460    async fn add_arc_size_constraint(
3461        &mut self,
3462        sketch: ObjectId,
3463        params: ArcSizeConstraintParams,
3464        new_ast: &mut ast::Node<ast::Program>,
3465    ) -> Result<AstNodeRef, KclError> {
3466        let sketch_id = sketch;
3467
3468        // Constraint must have exactly 1 argument (arc segment)
3469        if params.points.len() != 1 {
3470            return Err(KclError::refactor(format!(
3471                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3472                params.constraint_type_name,
3473                params.points.len()
3474            )));
3475        }
3476
3477        let arc_id = params.points[0];
3478        let arc_object = self
3479            .scene_graph
3480            .objects
3481            .get(arc_id.0)
3482            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3483        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3484            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3485        };
3486        let ref_type = match arc_segment {
3487            Segment::Arc(_) => ARC_VARIABLE,
3488            Segment::Circle(_) => CIRCLE_VARIABLE,
3489            _ => {
3490                return Err(KclError::refactor(format!(
3491                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3492                    params.constraint_type_name
3493                )));
3494            }
3495        };
3496        // Reference the arc/circle segment directly
3497        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3498        let arguments = match &params.label_position {
3499            Some(label_position) => vec![ast::LabeledArg {
3500                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3501                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3502            }],
3503            None => Default::default(),
3504        };
3505
3506        // Create the function call.
3507        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3508            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3509            unlabeled: Some(arc_ast),
3510            arguments,
3511            digest: None,
3512            non_code_meta: Default::default(),
3513        })));
3514        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3515            left: call_ast,
3516            operator: ast::BinaryOperator::Eq,
3517            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3518                value: ast::LiteralValue::Number {
3519                    value: params.value,
3520                    suffix: params.units,
3521                },
3522                raw: format_number_literal(params.value, params.units, None)
3523                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3524                digest: None,
3525            }))),
3526            digest: None,
3527        })));
3528
3529        // Add the line to the AST of the sketch block.
3530        let (sketch_block_ref, _) = self.mutate_ast(
3531            new_ast,
3532            sketch_id,
3533            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3534        )?;
3535        Ok(sketch_block_ref)
3536    }
3537
3538    async fn add_horizontal_distance(
3539        &mut self,
3540        sketch: ObjectId,
3541        distance: Distance,
3542        new_ast: &mut ast::Node<ast::Program>,
3543    ) -> Result<AstNodeRef, KclError> {
3544        let sketch_id = sketch;
3545        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3546            [pt0, pt1] => [
3547                self.coincident_segment_to_ast(pt0, new_ast)?,
3548                self.coincident_segment_to_ast(pt1, new_ast)?,
3549            ],
3550            _ => {
3551                return Err(KclError::refactor(format!(
3552                    "Horizontal distance constraint must have exactly 2 points, got {}",
3553                    distance.points.len()
3554                )));
3555            }
3556        };
3557
3558        let arguments = match &distance.label_position {
3559            Some(label_position) => vec![ast::LabeledArg {
3560                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3561                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3562            }],
3563            None => Default::default(),
3564        };
3565
3566        // Create the horizontalDistance() call.
3567        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3568            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3569            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3570                ast::ArrayExpression {
3571                    elements: vec![pt0_ast, pt1_ast],
3572                    digest: None,
3573                    non_code_meta: Default::default(),
3574                },
3575            )))),
3576            arguments,
3577            digest: None,
3578            non_code_meta: Default::default(),
3579        })));
3580        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3581            left: distance_call_ast,
3582            operator: ast::BinaryOperator::Eq,
3583            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3584                value: ast::LiteralValue::Number {
3585                    value: distance.distance.value,
3586                    suffix: distance.distance.units,
3587                },
3588                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3589                    KclError::refactor(format!(
3590                        "Could not format numeric suffix: {:?}",
3591                        distance.distance.units
3592                    ))
3593                })?,
3594                digest: None,
3595            }))),
3596            digest: None,
3597        })));
3598
3599        // Add the line to the AST of the sketch block.
3600        let (sketch_block_ref, _) = self.mutate_ast(
3601            new_ast,
3602            sketch_id,
3603            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3604        )?;
3605        Ok(sketch_block_ref)
3606    }
3607
3608    async fn add_vertical_distance(
3609        &mut self,
3610        sketch: ObjectId,
3611        distance: Distance,
3612        new_ast: &mut ast::Node<ast::Program>,
3613    ) -> Result<AstNodeRef, KclError> {
3614        let sketch_id = sketch;
3615        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3616            [pt0, pt1] => [
3617                self.coincident_segment_to_ast(pt0, new_ast)?,
3618                self.coincident_segment_to_ast(pt1, new_ast)?,
3619            ],
3620            _ => {
3621                return Err(KclError::refactor(format!(
3622                    "Vertical distance constraint must have exactly 2 points, got {}",
3623                    distance.points.len()
3624                )));
3625            }
3626        };
3627
3628        let arguments = match &distance.label_position {
3629            Some(label_position) => vec![ast::LabeledArg {
3630                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3631                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3632            }],
3633            None => Default::default(),
3634        };
3635
3636        // Create the verticalDistance() call.
3637        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3638            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3639            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3640                ast::ArrayExpression {
3641                    elements: vec![pt0_ast, pt1_ast],
3642                    digest: None,
3643                    non_code_meta: Default::default(),
3644                },
3645            )))),
3646            arguments,
3647            digest: None,
3648            non_code_meta: Default::default(),
3649        })));
3650        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3651            left: distance_call_ast,
3652            operator: ast::BinaryOperator::Eq,
3653            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3654                value: ast::LiteralValue::Number {
3655                    value: distance.distance.value,
3656                    suffix: distance.distance.units,
3657                },
3658                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3659                    KclError::refactor(format!(
3660                        "Could not format numeric suffix: {:?}",
3661                        distance.distance.units
3662                    ))
3663                })?,
3664                digest: None,
3665            }))),
3666            digest: None,
3667        })));
3668
3669        // Add the line to the AST of the sketch block.
3670        let (sketch_block_ref, _) = self.mutate_ast(
3671            new_ast,
3672            sketch_id,
3673            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3674        )?;
3675        Ok(sketch_block_ref)
3676    }
3677
3678    async fn add_horizontal(
3679        &mut self,
3680        sketch: ObjectId,
3681        horizontal: Horizontal,
3682        new_ast: &mut ast::Node<ast::Program>,
3683    ) -> Result<AstNodeRef, KclError> {
3684        let sketch_id = sketch;
3685
3686        // Map the runtime objects back to variable names.
3687        let first_arg_ast = match horizontal {
3688            Horizontal::Line { line } => {
3689                let line_object = self
3690                    .scene_graph
3691                    .objects
3692                    .get(line.0)
3693                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3694                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3695                    let kind = line_object.kind.human_friendly_kind_with_article();
3696                    return Err(KclError::refactor(format!(
3697                        "This constraint only works on Segments, but you selected {kind}"
3698                    )));
3699                };
3700                let Segment::Line(_) = line_segment else {
3701                    return Err(KclError::refactor(format!(
3702                        "Only lines can be made horizontal, but you selected {}",
3703                        line_segment.human_friendly_kind_with_article(),
3704                    )));
3705                };
3706                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3707            }
3708            Horizontal::Points { points } => {
3709                let point_asts = points
3710                    .iter()
3711                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3712                    .collect::<Result<Vec<_>, _>>()?;
3713                ast::ArrayExpression::new(point_asts).into()
3714            }
3715        };
3716
3717        // Create the horizontal() call using shared helper.
3718        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3719
3720        // Add the line to the AST of the sketch block.
3721        let (sketch_block_ref, _) = self.mutate_ast(
3722            new_ast,
3723            sketch_id,
3724            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3725        )?;
3726        Ok(sketch_block_ref)
3727    }
3728
3729    async fn add_lines_equal_length(
3730        &mut self,
3731        sketch: ObjectId,
3732        lines_equal_length: LinesEqualLength,
3733        new_ast: &mut ast::Node<ast::Program>,
3734    ) -> Result<AstNodeRef, KclError> {
3735        if lines_equal_length.lines.len() < 2 {
3736            return Err(KclError::refactor(format!(
3737                "Lines equal length constraint must have at least 2 lines, got {}",
3738                lines_equal_length.lines.len()
3739            )));
3740        };
3741
3742        let sketch_id = sketch;
3743
3744        // Map the runtime objects back to variable names.
3745        let line_asts = lines_equal_length
3746            .lines
3747            .iter()
3748            .map(|line_id| {
3749                let line_object = self
3750                    .scene_graph
3751                    .objects
3752                    .get(line_id.0)
3753                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3754                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3755                    let kind = line_object.kind.human_friendly_kind_with_article();
3756                    return Err(KclError::refactor(format!(
3757                        "This constraint only works on Segments, but you selected {kind}"
3758                    )));
3759                };
3760                let Segment::Line(_) = line_segment else {
3761                    let kind = line_segment.human_friendly_kind_with_article();
3762                    return Err(KclError::refactor(format!(
3763                        "Only lines can be made equal length, but you selected {kind}"
3764                    )));
3765                };
3766
3767                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3768            })
3769            .collect::<Result<Vec<_>, _>>()?;
3770
3771        // Create the equalLength() call using shared helper.
3772        let equal_length_ast = create_equal_length_ast(line_asts);
3773
3774        // Add the constraint to the AST of the sketch block.
3775        let (sketch_block_ref, _) = self.mutate_ast(
3776            new_ast,
3777            sketch_id,
3778            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3779        )?;
3780        Ok(sketch_block_ref)
3781    }
3782
3783    fn equal_radius_segment_id_to_ast_reference(
3784        &mut self,
3785        segment_id: ObjectId,
3786        new_ast: &mut ast::Node<ast::Program>,
3787    ) -> Result<ast::Expr, KclError> {
3788        let segment_object = self
3789            .scene_graph
3790            .objects
3791            .get(segment_id.0)
3792            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3793        let ObjectKind::Segment { segment } = &segment_object.kind else {
3794            return Err(KclError::refactor(format!(
3795                "Object is not a segment, it was {}",
3796                segment_object.kind.human_friendly_kind_with_article()
3797            )));
3798        };
3799
3800        let ref_type = match segment {
3801            Segment::Arc(_) => ARC_VARIABLE,
3802            Segment::Circle(_) => CIRCLE_VARIABLE,
3803            _ => {
3804                return Err(KclError::refactor(format!(
3805                    "equalRadius supports only arc/circle segments, got {}",
3806                    segment.human_friendly_kind_with_article()
3807                )));
3808            }
3809        };
3810
3811        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3812    }
3813
3814    fn symmetric_input_id_to_ast_reference(
3815        &mut self,
3816        segment_id: ObjectId,
3817        new_ast: &mut ast::Node<ast::Program>,
3818    ) -> Result<ast::Expr, KclError> {
3819        let segment_object = self
3820            .scene_graph
3821            .objects
3822            .get(segment_id.0)
3823            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3824        let ObjectKind::Segment { segment } = &segment_object.kind else {
3825            return Err(KclError::refactor(format!(
3826                "Object is not a segment, it was {}",
3827                segment_object.kind.human_friendly_kind_with_article()
3828            )));
3829        };
3830
3831        match segment {
3832            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3833            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3834            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3835            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3836        }
3837    }
3838
3839    fn symmetric_axis_id_to_ast_reference(
3840        &mut self,
3841        segment_id: ObjectId,
3842        new_ast: &mut ast::Node<ast::Program>,
3843    ) -> Result<ast::Expr, KclError> {
3844        let segment_object = self
3845            .scene_graph
3846            .objects
3847            .get(segment_id.0)
3848            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3849        let ObjectKind::Segment { segment } = &segment_object.kind else {
3850            return Err(KclError::refactor(format!(
3851                "Object is not a segment, it was {}",
3852                segment_object.kind.human_friendly_kind_with_article()
3853            )));
3854        };
3855        match segment {
3856            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3857            _ => Err(KclError::refactor(format!(
3858                "Symmetric axis must be a line, got {}",
3859                segment.human_friendly_kind_with_article()
3860            ))),
3861        }
3862    }
3863
3864    async fn add_parallel(
3865        &mut self,
3866        sketch: ObjectId,
3867        parallel: Parallel,
3868        new_ast: &mut ast::Node<ast::Program>,
3869    ) -> Result<AstNodeRef, KclError> {
3870        if parallel.lines.len() < 2 {
3871            return Err(KclError::refactor(format!(
3872                "Parallel constraint must have at least 2 lines, got {}",
3873                parallel.lines.len()
3874            )));
3875        };
3876
3877        let sketch_id = sketch;
3878
3879        let line_asts = parallel
3880            .lines
3881            .iter()
3882            .map(|line_id| {
3883                let line_object = self
3884                    .scene_graph
3885                    .objects
3886                    .get(line_id.0)
3887                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3888                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3889                    let kind = line_object.kind.human_friendly_kind_with_article();
3890                    return Err(KclError::refactor(format!(
3891                        "This constraint only works on Segments, but you selected {kind}"
3892                    )));
3893                };
3894                let Segment::Line(_) = line_segment else {
3895                    let kind = line_segment.human_friendly_kind_with_article();
3896                    return Err(KclError::refactor(format!(
3897                        "Only lines can be made parallel, but you selected {kind}"
3898                    )));
3899                };
3900
3901                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3902            })
3903            .collect::<Result<Vec<_>, _>>()?;
3904
3905        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3906            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3907            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3908                ast::ArrayExpression {
3909                    elements: line_asts,
3910                    digest: None,
3911                    non_code_meta: Default::default(),
3912                },
3913            )))),
3914            arguments: Default::default(),
3915            digest: None,
3916            non_code_meta: Default::default(),
3917        })));
3918
3919        let (sketch_block_ref, _) = self.mutate_ast(
3920            new_ast,
3921            sketch_id,
3922            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3923        )?;
3924        Ok(sketch_block_ref)
3925    }
3926
3927    async fn add_perpendicular(
3928        &mut self,
3929        sketch: ObjectId,
3930        perpendicular: Perpendicular,
3931        new_ast: &mut ast::Node<ast::Program>,
3932    ) -> Result<AstNodeRef, KclError> {
3933        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3934            .await
3935    }
3936
3937    async fn add_lines_at_angle_constraint(
3938        &mut self,
3939        sketch: ObjectId,
3940        angle_kind: LinesAtAngleKind,
3941        lines: Vec<ObjectId>,
3942        new_ast: &mut ast::Node<ast::Program>,
3943    ) -> Result<AstNodeRef, KclError> {
3944        let &[line0_id, line1_id] = lines.as_slice() else {
3945            return Err(KclError::refactor(format!(
3946                "{} constraint must have exactly 2 lines, got {}",
3947                angle_kind.to_function_name(),
3948                lines.len()
3949            )));
3950        };
3951
3952        let sketch_id = sketch;
3953
3954        // Map the runtime objects back to variable names.
3955        let line0_object = self
3956            .scene_graph
3957            .objects
3958            .get(line0_id.0)
3959            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3960        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3961            let kind = line0_object.kind.human_friendly_kind_with_article();
3962            return Err(KclError::refactor(format!(
3963                "This constraint only works on Segments, but you selected {kind}"
3964            )));
3965        };
3966        let Segment::Line(_) = line0_segment else {
3967            return Err(KclError::refactor(format!(
3968                "Only lines can be made {}, but you selected {}",
3969                angle_kind.to_function_name(),
3970                line0_segment.human_friendly_kind_with_article(),
3971            )));
3972        };
3973        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3974
3975        let line1_object = self
3976            .scene_graph
3977            .objects
3978            .get(line1_id.0)
3979            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3980        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3981            let kind = line1_object.kind.human_friendly_kind_with_article();
3982            return Err(KclError::refactor(format!(
3983                "This constraint only works on Segments, but you selected {kind}"
3984            )));
3985        };
3986        let Segment::Line(_) = line1_segment else {
3987            return Err(KclError::refactor(format!(
3988                "Only lines can be made {}, but you selected {}",
3989                angle_kind.to_function_name(),
3990                line1_segment.human_friendly_kind_with_article(),
3991            )));
3992        };
3993        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3994
3995        // Create the parallel() or perpendicular() call.
3996        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3997            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
3998            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3999                ast::ArrayExpression {
4000                    elements: vec![line0_ast, line1_ast],
4001                    digest: None,
4002                    non_code_meta: Default::default(),
4003                },
4004            )))),
4005            arguments: Default::default(),
4006            digest: None,
4007            non_code_meta: Default::default(),
4008        })));
4009
4010        // Add the constraint to the AST of the sketch block.
4011        let (sketch_block_ref, _) = self.mutate_ast(
4012            new_ast,
4013            sketch_id,
4014            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4015        )?;
4016        Ok(sketch_block_ref)
4017    }
4018
4019    async fn add_vertical(
4020        &mut self,
4021        sketch: ObjectId,
4022        vertical: Vertical,
4023        new_ast: &mut ast::Node<ast::Program>,
4024    ) -> Result<AstNodeRef, KclError> {
4025        let sketch_id = sketch;
4026
4027        let first_arg_ast = match vertical {
4028            Vertical::Line { line } => {
4029                // Map the runtime objects back to variable names.
4030                let line_object = self
4031                    .scene_graph
4032                    .objects
4033                    .get(line.0)
4034                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4035                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4036                    let kind = line_object.kind.human_friendly_kind_with_article();
4037                    return Err(KclError::refactor(format!(
4038                        "This constraint only works on Segments, but you selected {kind}"
4039                    )));
4040                };
4041                let Segment::Line(_) = line_segment else {
4042                    return Err(KclError::refactor(format!(
4043                        "Only lines can be made vertical, but you selected {}",
4044                        line_segment.human_friendly_kind_with_article()
4045                    )));
4046                };
4047                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4048            }
4049            Vertical::Points { points } => {
4050                let point_asts = points
4051                    .iter()
4052                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4053                    .collect::<Result<Vec<_>, _>>()?;
4054                ast::ArrayExpression::new(point_asts).into()
4055            }
4056        };
4057
4058        // Create the vertical() call using shared helper.
4059        let vertical_ast = create_vertical_ast(first_arg_ast);
4060
4061        // Add the line to the AST of the sketch block.
4062        let (sketch_block_ref, _) = self.mutate_ast(
4063            new_ast,
4064            sketch_id,
4065            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4066        )?;
4067        Ok(sketch_block_ref)
4068    }
4069
4070    async fn execute_after_add_constraint(
4071        &mut self,
4072        ctx: &ExecutorContext,
4073        sketch_id: ObjectId,
4074        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4075        new_ast: &mut ast::Node<ast::Program>,
4076    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4077        // Convert to string source to create real source ranges.
4078        let new_source = source_from_ast(new_ast);
4079        // Parse the new KCL source.
4080        let (new_program, errors) = Program::parse(&new_source)
4081            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4082        if !errors.is_empty() {
4083            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4084                "Error parsing KCL source after adding constraint: {errors:?}"
4085            ))));
4086        }
4087        let Some(new_program) = new_program else {
4088            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4089                "No AST produced after adding constraint".to_string(),
4090            )));
4091        };
4092        #[cfg(feature = "artifact-graph")]
4093        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4094            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4095                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4096            )))
4097        })?;
4098
4099        // Truncate after the sketch block for mock execution.
4100        // Use a clone so we don't mutate new_program yet
4101        let mut truncated_program = new_program.clone();
4102        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4103            .map_err(KclErrorWithOutputs::no_outputs)?;
4104
4105        // Execute - if this fails, we haven't modified self yet, so state is safe
4106        let outcome = ctx
4107            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4108            .await?;
4109
4110        #[cfg(not(feature = "artifact-graph"))]
4111        let new_object_ids = Vec::new();
4112        #[cfg(feature = "artifact-graph")]
4113        let new_object_ids = {
4114            // Extract the constraint ID from the execution outcome using source_range_to_object
4115            let constraint_id = outcome
4116                .source_range_to_object
4117                .get(&constraint_node_ref.range)
4118                .copied()
4119                .ok_or_else(|| {
4120                    KclErrorWithOutputs::from_error_outcome(
4121                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4122                        outcome.clone(),
4123                    )
4124                })?;
4125            vec![constraint_id]
4126        };
4127
4128        // Only now, after all operations succeeded, update self.program
4129        // This ensures state is only modified if everything succeeds
4130        self.program = new_program;
4131
4132        // Uses MockConfig::default() which has freedom_analysis: true
4133        let outcome = self.update_state_after_exec(outcome, true);
4134
4135        let src_delta = SourceDelta { text: new_source };
4136        let scene_graph_delta = SceneGraphDelta {
4137            new_graph: self.scene_graph.clone(),
4138            invalidates_ids: false,
4139            new_objects: new_object_ids,
4140            exec_outcome: outcome,
4141        };
4142        Ok((src_delta, scene_graph_delta))
4143    }
4144
4145    // Find constraints that reference the given segments.
4146    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4147        if segment_ids_set.contains(&segment_id) {
4148            return true;
4149        }
4150
4151        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4152            return false;
4153        };
4154        let ObjectKind::Segment { segment } = &segment_object.kind else {
4155            return false;
4156        };
4157        let Segment::Point(point) = segment else {
4158            return false;
4159        };
4160
4161        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4162    }
4163
4164    fn remaining_constraint_segments(
4165        &self,
4166        segments: &[ConstraintSegment],
4167        segment_ids_set: &AhashIndexSet<ObjectId>,
4168    ) -> Vec<ConstraintSegment> {
4169        segments
4170            .iter()
4171            .copied()
4172            .filter(|segment| match segment {
4173                ConstraintSegment::Origin(_) => true,
4174                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4175            })
4176            .collect()
4177    }
4178
4179    fn find_referenced_constraints(
4180        &self,
4181        sketch_id: ObjectId,
4182        segment_ids_set: &AhashIndexSet<ObjectId>,
4183    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4184        // Look up the sketch.
4185        let sketch_object = self
4186            .scene_graph
4187            .objects
4188            .get(sketch_id.0)
4189            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4190        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4191            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4192        };
4193        let mut constraint_ids_set = AhashIndexSet::default();
4194        for constraint_id in &sketch.constraints {
4195            let constraint_object = self
4196                .scene_graph
4197                .objects
4198                .get(constraint_id.0)
4199                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4200            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4201                return Err(KclError::refactor(format!(
4202                    "Object is not a constraint, it is {}",
4203                    constraint_object.kind.human_friendly_kind_with_article()
4204                )));
4205            };
4206            let depends_on_segment = match constraint {
4207                Constraint::Coincident(c) => c
4208                    .segment_ids()
4209                    .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4210                Constraint::Distance(d) => d
4211                    .point_ids()
4212                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4213                Constraint::Fixed(fixed) => fixed
4214                    .points
4215                    .iter()
4216                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4217                Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4218                Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4219                Constraint::EqualRadius(equal_radius) => equal_radius
4220                    .input
4221                    .iter()
4222                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4223                Constraint::HorizontalDistance(d) => d
4224                    .point_ids()
4225                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4226                Constraint::VerticalDistance(d) => d
4227                    .point_ids()
4228                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4229                Constraint::Horizontal(h) => match h {
4230                    Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4231                    Horizontal::Points { points } => points.iter().any(|point| match point {
4232                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4233                        ConstraintSegment::Origin(_) => false,
4234                    }),
4235                },
4236                Constraint::Vertical(v) => match v {
4237                    Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4238                    Vertical::Points { points } => points.iter().any(|point| match point {
4239                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4240                        ConstraintSegment::Origin(_) => false,
4241                    }),
4242                },
4243                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4244                    .lines
4245                    .iter()
4246                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4247                Constraint::Midpoint(midpoint) => {
4248                    self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4249                        || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4250                }
4251                Constraint::Parallel(parallel) => parallel
4252                    .lines
4253                    .iter()
4254                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4255                Constraint::Perpendicular(perpendicular) => perpendicular
4256                    .lines
4257                    .iter()
4258                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4259                Constraint::Angle(angle) => angle
4260                    .lines
4261                    .iter()
4262                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4263                Constraint::Symmetric(symmetric) => {
4264                    self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4265                        || symmetric
4266                            .input
4267                            .iter()
4268                            .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4269                }
4270                Constraint::Tangent(tangent) => tangent
4271                    .input
4272                    .iter()
4273                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4274            };
4275            if depends_on_segment {
4276                constraint_ids_set.insert(*constraint_id);
4277            }
4278        }
4279        Ok(constraint_ids_set)
4280    }
4281
4282    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4283        #[cfg(not(feature = "artifact-graph"))]
4284        {
4285            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
4286            outcome
4287        }
4288        #[cfg(feature = "artifact-graph")]
4289        {
4290            let mut outcome = outcome;
4291            let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4292
4293            if freedom_analysis_ran {
4294                // When freedom analysis ran, replace the cache entirely with new values
4295                // Don't merge with old values since IDs might have changed
4296                self.point_freedom_cache.clear();
4297                for new_obj in &new_objects {
4298                    if let ObjectKind::Segment {
4299                        segment: crate::front::Segment::Point(point),
4300                    } = &new_obj.kind
4301                    {
4302                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
4303                    }
4304                }
4305                add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4306                // Objects are already correct from the analysis, just use them as-is
4307                self.scene_graph.objects = new_objects;
4308            } else {
4309                // When freedom analysis didn't run, preserve old values and merge
4310                // Before replacing objects, extract and store freedom values from old objects
4311                for old_obj in &self.scene_graph.objects {
4312                    if let ObjectKind::Segment {
4313                        segment: crate::front::Segment::Point(point),
4314                    } = &old_obj.kind
4315                    {
4316                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
4317                    }
4318                }
4319
4320                // Update objects, preserving stored freedom values when new is Free (might be default)
4321                let mut updated_objects = Vec::with_capacity(new_objects.len());
4322                for new_obj in new_objects {
4323                    let mut obj = new_obj;
4324                    if let ObjectKind::Segment {
4325                        segment: crate::front::Segment::Point(point),
4326                    } = &mut obj.kind
4327                    {
4328                        let new_freedom = point.freedom;
4329                        // When freedom_analysis=false, new values are defaults (Free).
4330                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4331                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4332                        // Never preserve Conflict from cache - conflicts are transient and should only be set
4333                        // when there are actually unsatisfied constraints.
4334                        match new_freedom {
4335                            Freedom::Free => {
4336                                match self.point_freedom_cache.get(&obj.id).copied() {
4337                                    Some(Freedom::Conflict) => {
4338                                        // Don't preserve Conflict - conflicts are transient
4339                                        // Keep it as Free
4340                                    }
4341                                    Some(Freedom::Fixed) => {
4342                                        // Preserve Fixed cached value
4343                                        point.freedom = Freedom::Fixed;
4344                                    }
4345                                    Some(Freedom::Free) => {
4346                                        // If stored is also Free, keep Free (no change needed)
4347                                    }
4348                                    None => {
4349                                        // If no cached value, keep Free (default)
4350                                    }
4351                                }
4352                            }
4353                            Freedom::Fixed => {
4354                                // Use new value (already set)
4355                            }
4356                            Freedom::Conflict => {
4357                                // Use new value (already set)
4358                            }
4359                        }
4360                        // Store the new freedom value (even if it's Free, so we know it was set)
4361                        self.point_freedom_cache.insert(obj.id, point.freedom);
4362                    }
4363                    updated_objects.push(obj);
4364                }
4365
4366                add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4367                self.scene_graph.objects = updated_objects;
4368            }
4369            outcome
4370        }
4371    }
4372
4373    fn mutate_ast(
4374        &mut self,
4375        ast: &mut ast::Node<ast::Program>,
4376        object_id: ObjectId,
4377        command: AstMutateCommand,
4378    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4379        let sketch_object = self
4380            .scene_graph
4381            .objects
4382            .get(object_id.0)
4383            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4384        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4385    }
4386}
4387
4388fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4389    // Look up existing sketch.
4390    let sketch_object = scene_graph
4391        .objects
4392        .get(sketch_id.0)
4393        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4394    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4395        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4396    };
4397    expect_single_node_ref(sketch_object)
4398}
4399
4400fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4401    match &object.source {
4402        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4403            range: *range,
4404            node_path: node_path.clone(),
4405        }),
4406        SourceRef::BackTrace { ranges } => {
4407            let [range] = ranges.as_slice() else {
4408                return Err(KclError::refactor(format!(
4409                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4410                    ranges.len()
4411                )));
4412            };
4413            Ok(AstNodeRef {
4414                range: range.0,
4415                node_path: range.1.clone(),
4416            })
4417        }
4418    }
4419}
4420
4421/// This is a deprecated fall-back implementation. Prefer
4422/// [`only_sketch_block()`] to avoid reliance on source ranges.
4423fn only_sketch_block_from_range(
4424    ast: &mut ast::Node<ast::Program>,
4425    sketch_block_range: SourceRange,
4426    edit_kind: ChangeKind,
4427) -> Result<(), KclError> {
4428    let r1 = sketch_block_range;
4429    let matches_range = |r2: SourceRange| -> bool {
4430        // We may have added items to the sketch block, so the end may not be an
4431        // exact match.
4432        match edit_kind {
4433            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4434            // For edit, we don't know whether it grew or shrank.
4435            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4436            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4437            // No edit should be an exact match.
4438            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4439        }
4440    };
4441    let mut found = false;
4442    for item in ast.body.iter_mut() {
4443        match item {
4444            ast::BodyItem::ImportStatement(_) => {}
4445            ast::BodyItem::ExpressionStatement(node) => {
4446                if matches_range(SourceRange::from(&*node))
4447                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4448                {
4449                    sketch_block.is_being_edited = true;
4450                    found = true;
4451                    break;
4452                }
4453            }
4454            ast::BodyItem::VariableDeclaration(node) => {
4455                if matches_range(SourceRange::from(&node.declaration.init))
4456                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4457                {
4458                    sketch_block.is_being_edited = true;
4459                    found = true;
4460                    break;
4461                }
4462            }
4463            ast::BodyItem::TypeDeclaration(_) => {}
4464            ast::BodyItem::ReturnStatement(node) => {
4465                if matches_range(SourceRange::from(&node.argument))
4466                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4467                {
4468                    sketch_block.is_being_edited = true;
4469                    found = true;
4470                    break;
4471                }
4472            }
4473        }
4474    }
4475    if !found {
4476        return Err(KclError::refactor(format!(
4477            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4478        )));
4479    }
4480
4481    Ok(())
4482}
4483
4484fn only_sketch_block(
4485    ast: &mut ast::Node<ast::Program>,
4486    sketch_block_ref: &AstNodeRef,
4487    edit_kind: ChangeKind,
4488) -> Result<(), KclError> {
4489    let Some(target_node_path) = &sketch_block_ref.node_path else {
4490        #[cfg(target_arch = "wasm32")]
4491        web_sys::console::warn_1(
4492            &format!(
4493                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4494                &sketch_block_ref
4495            )
4496            .into(),
4497        );
4498        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4499    };
4500    let mut found = false;
4501    for item in ast.body.iter_mut() {
4502        match item {
4503            ast::BodyItem::ImportStatement(_) => {}
4504            ast::BodyItem::ExpressionStatement(node) => {
4505                // Check the statement.
4506                if let Some(node_path) = &node.node_path
4507                    && node_path == target_node_path
4508                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4509                {
4510                    sketch_block.is_being_edited = true;
4511                    found = true;
4512                    break;
4513                }
4514                // Check the expression.
4515                if let Some(node_path) = node.expression.node_path()
4516                    && node_path == target_node_path
4517                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4518                {
4519                    sketch_block.is_being_edited = true;
4520                    found = true;
4521                    break;
4522                }
4523            }
4524            ast::BodyItem::VariableDeclaration(node) => {
4525                if let Some(node_path) = node.declaration.init.node_path()
4526                    && node_path == target_node_path
4527                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4528                {
4529                    sketch_block.is_being_edited = true;
4530                    found = true;
4531                    break;
4532                }
4533            }
4534            ast::BodyItem::TypeDeclaration(_) => {}
4535            ast::BodyItem::ReturnStatement(node) => {
4536                if let Some(node_path) = node.argument.node_path()
4537                    && node_path == target_node_path
4538                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4539                {
4540                    sketch_block.is_being_edited = true;
4541                    found = true;
4542                    break;
4543                }
4544            }
4545        }
4546    }
4547    if !found {
4548        return Err(KclError::refactor(format!(
4549            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4550        )));
4551    }
4552
4553    Ok(())
4554}
4555
4556fn sketch_on_ast_expr(
4557    ast: &mut ast::Node<ast::Program>,
4558    scene_graph: &SceneGraph,
4559    on: &Plane,
4560) -> Result<ast::Expr, KclError> {
4561    match on {
4562        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4563        Plane::Object(object_id) => {
4564            let on_object = scene_graph
4565                .objects
4566                .get(object_id.0)
4567                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4568            #[cfg(feature = "artifact-graph")]
4569            {
4570                if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4571                    return Ok(face_expr);
4572                }
4573            }
4574            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4575        }
4576    }
4577}
4578
4579#[cfg(feature = "artifact-graph")]
4580fn sketch_face_of_scene_object_ast_expr(
4581    ast: &mut ast::Node<ast::Program>,
4582    on_object: &crate::front::Object,
4583) -> Result<Option<ast::Expr>, KclError> {
4584    let SourceRef::BackTrace { ranges } = &on_object.source else {
4585        return Ok(None);
4586    };
4587
4588    match &on_object.kind {
4589        ObjectKind::Wall(_) => {
4590            let [sweep_range, segment_range] = ranges.as_slice() else {
4591                return Err(KclError::refactor(format!(
4592                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4593                    ranges.len(),
4594                    on_object.artifact_id
4595                )));
4596            };
4597            let sweep_ref = get_or_insert_ast_reference(
4598                ast,
4599                &SourceRef::Simple {
4600                    range: sweep_range.0,
4601                    node_path: sweep_range.1.clone(),
4602                },
4603                "solid",
4604                None,
4605            )?;
4606            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4607                return Err(KclError::refactor(format!(
4608                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4609                    on_object.artifact_id
4610                )));
4611            };
4612            let solid_name = solid_name_expr.name.name.clone();
4613            let solid_expr = ast_name_expr(solid_name.clone());
4614            let segment_ref = get_or_insert_ast_reference(
4615                ast,
4616                &SourceRef::Simple {
4617                    range: segment_range.0,
4618                    node_path: segment_range.1.clone(),
4619                },
4620                LINE_VARIABLE,
4621                None,
4622            )?;
4623
4624            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4625                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4626                    return Err(KclError::refactor(format!(
4627                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4628                        on_object.artifact_id
4629                    )));
4630                };
4631                create_member_expression(
4632                    create_member_expression(ast_name_expr(region_name), "tags"),
4633                    &segment_name_expr.name.name,
4634                )
4635            } else {
4636                segment_ref
4637            };
4638
4639            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4640        }
4641        ObjectKind::Cap(cap) => {
4642            let [range] = ranges.as_slice() else {
4643                return Err(KclError::refactor(format!(
4644                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4645                    ranges.len(),
4646                    on_object.artifact_id
4647                )));
4648            };
4649            let sweep_ref = get_or_insert_ast_reference(
4650                ast,
4651                &SourceRef::Simple {
4652                    range: range.0,
4653                    node_path: range.1.clone(),
4654                },
4655                "solid",
4656                None,
4657            )?;
4658            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4659                return Err(KclError::refactor(format!(
4660                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4661                    on_object.artifact_id
4662                )));
4663            };
4664            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4665            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4666            let face_expr = match cap.kind {
4667                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4668                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4669            };
4670
4671            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4672        }
4673        _ => Ok(None),
4674    }
4675}
4676
4677#[cfg(feature = "artifact-graph")]
4678fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4679    let mut existing_artifact_ids = scene_objects
4680        .iter()
4681        .map(|object| object.artifact_id)
4682        .collect::<HashSet<_>>();
4683
4684    for artifact in artifact_graph.values() {
4685        match artifact {
4686            Artifact::Wall(wall) => {
4687                if existing_artifact_ids.contains(&wall.id) {
4688                    continue;
4689                }
4690
4691                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4692                    Artifact::Segment(segment) => Some(segment),
4693                    _ => None,
4694                }) else {
4695                    continue;
4696                };
4697                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4698                    Artifact::Sweep(sweep) => Some(sweep),
4699                    _ => None,
4700                }) else {
4701                    continue;
4702                };
4703                let source_segment = segment
4704                    .original_seg_id
4705                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4706                    .and_then(|artifact| match artifact {
4707                        Artifact::Segment(segment) => Some(segment),
4708                        _ => None,
4709                    })
4710                    .unwrap_or(segment);
4711                let id = ObjectId(scene_objects.len());
4712                scene_objects.push(crate::front::Object {
4713                    id,
4714                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4715                    label: Default::default(),
4716                    comments: Default::default(),
4717                    artifact_id: wall.id,
4718                    source: SourceRef::BackTrace {
4719                        ranges: vec![
4720                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4721                            (
4722                                source_segment.code_ref.range,
4723                                Some(source_segment.code_ref.node_path.clone()),
4724                            ),
4725                        ],
4726                    },
4727                });
4728                existing_artifact_ids.insert(wall.id);
4729            }
4730            Artifact::Cap(cap) => {
4731                if existing_artifact_ids.contains(&cap.id) {
4732                    continue;
4733                }
4734
4735                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4736                    Artifact::Sweep(sweep) => Some(sweep),
4737                    _ => None,
4738                }) else {
4739                    continue;
4740                };
4741                let id = ObjectId(scene_objects.len());
4742                let kind = match cap.sub_type {
4743                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4744                    CapSubType::End => crate::frontend::api::CapKind::End,
4745                };
4746                scene_objects.push(crate::front::Object {
4747                    id,
4748                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4749                    label: Default::default(),
4750                    comments: Default::default(),
4751                    artifact_id: cap.id,
4752                    source: SourceRef::BackTrace {
4753                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4754                    },
4755                });
4756                existing_artifact_ids.insert(cap.id);
4757            }
4758            _ => {}
4759        }
4760    }
4761}
4762
4763fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4764    use crate::engine::PlaneName;
4765
4766    match name {
4767        PlaneName::Xy => ast_name_expr("XY".to_owned()),
4768        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4769        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4770        PlaneName::NegXy => negated_plane_ast_expr("XY"),
4771        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4772        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4773    }
4774}
4775
4776fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4777    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4778        ast::UnaryOperator::Neg,
4779        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4780    )))
4781}
4782
4783#[cfg(feature = "artifact-graph")]
4784fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4785    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4786        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4787        unlabeled: Some(solid_expr),
4788        arguments: vec![ast::LabeledArg {
4789            label: Some(ast::Identifier::new("face")),
4790            arg: face_expr,
4791        }],
4792        digest: None,
4793        non_code_meta: Default::default(),
4794    })))
4795}
4796
4797#[cfg(feature = "artifact-graph")]
4798fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4799    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4800        return None;
4801    };
4802    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4803        return None;
4804    };
4805    if !matches!(
4806        sweep_call.callee.name.name.as_str(),
4807        "extrude" | "revolve" | "sweep" | "loft"
4808    ) {
4809        return None;
4810    }
4811    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4812        return None;
4813    };
4814    let candidate = region_name_expr.name.name.clone();
4815    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4816        return None;
4817    };
4818    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
4819        return None;
4820    };
4821    if region_call.callee.name.name != "region" {
4822        return None;
4823    }
4824    Some(candidate)
4825}
4826
4827/// Return the AST expression referencing the variable at the given source ref.
4828/// If no such variable exists, insert a new variable declaration with the given
4829/// prefix.
4830///
4831/// This may return a complex expression referencing properties of the variable
4832/// (e.g., `line1.start`).
4833fn get_or_insert_ast_reference(
4834    ast: &mut ast::Node<ast::Program>,
4835    source_ref: &SourceRef,
4836    prefix: &str,
4837    property: Option<&str>,
4838) -> Result<ast::Expr, KclError> {
4839    let command = AstMutateCommand::AddVariableDeclaration {
4840        prefix: prefix.to_owned(),
4841    };
4842    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4843    let AstMutateCommandReturn::Name(var_name) = ret else {
4844        return Err(KclError::refactor(
4845            "Expected variable name returned from AddVariableDeclaration".to_owned(),
4846        ));
4847    };
4848    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4849    let Some(property) = property else {
4850        // No property; just return the variable name.
4851        return Ok(var_expr);
4852    };
4853
4854    Ok(create_member_expression(var_expr, property))
4855}
4856
4857fn mutate_ast_node_by_source_ref(
4858    ast: &mut ast::Node<ast::Program>,
4859    source_ref: &SourceRef,
4860    command: AstMutateCommand,
4861) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4862    let (source_range, node_path) = match source_ref {
4863        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4864        SourceRef::BackTrace { ranges } => {
4865            let [range] = ranges.as_slice() else {
4866                return Err(KclError::refactor(format!(
4867                    "Expected single source ref, got {}; ranges={ranges:#?}",
4868                    ranges.len(),
4869                )));
4870            };
4871            (range.0, range.1.clone())
4872        }
4873    };
4874    let mut context = AstMutateContext {
4875        source_range,
4876        node_path,
4877        command,
4878        defined_names_stack: Default::default(),
4879    };
4880    let control = dfs_mut(ast, &mut context);
4881    match control {
4882        ControlFlow::Continue(_) => Err(KclError::refactor(
4883            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4884        )),
4885        ControlFlow::Break(break_value) => break_value,
4886    }
4887}
4888
4889#[derive(Debug)]
4890struct AstMutateContext {
4891    source_range: SourceRange,
4892    node_path: Option<ast::NodePath>,
4893    command: AstMutateCommand,
4894    defined_names_stack: Vec<HashSet<String>>,
4895}
4896
4897#[derive(Debug)]
4898#[allow(clippy::large_enum_variant)]
4899enum AstMutateCommand {
4900    /// Add an expression statement to the sketch block.
4901    AddSketchBlockExprStmt {
4902        expr: ast::Expr,
4903    },
4904    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
4905    AddSketchBlockVarDecl {
4906        prefix: String,
4907        expr: ast::Expr,
4908    },
4909    AddVariableDeclaration {
4910        prefix: String,
4911    },
4912    EditPoint {
4913        at: ast::Expr,
4914    },
4915    EditLine {
4916        start: ast::Expr,
4917        end: ast::Expr,
4918        construction: Option<bool>,
4919    },
4920    EditArc {
4921        start: ast::Expr,
4922        end: ast::Expr,
4923        center: ast::Expr,
4924        construction: Option<bool>,
4925    },
4926    EditCircle {
4927        start: ast::Expr,
4928        center: ast::Expr,
4929        construction: Option<bool>,
4930    },
4931    EditConstraintValue {
4932        value: ast::BinaryPart,
4933    },
4934    EditDistanceConstraintLabelPosition {
4935        label_position: ast::Expr,
4936    },
4937    EditCallUnlabeled {
4938        arg: ast::Expr,
4939    },
4940    #[cfg(feature = "artifact-graph")]
4941    EditVarInitialValue {
4942        value: Number,
4943    },
4944    DeleteNode,
4945}
4946
4947impl AstMutateCommand {
4948    fn needs_defined_names_stack(&self) -> bool {
4949        matches!(
4950            self,
4951            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4952        )
4953    }
4954}
4955
4956#[derive(Debug)]
4957enum AstMutateCommandReturn {
4958    None,
4959    Name(String),
4960}
4961
4962#[derive(Debug, Clone)]
4963struct AstNodeRef {
4964    range: SourceRange,
4965    node_path: Option<ast::NodePath>,
4966}
4967
4968impl<T> From<&ast::Node<T>> for AstNodeRef {
4969    fn from(value: &ast::Node<T>) -> Self {
4970        AstNodeRef {
4971            range: value.into(),
4972            node_path: value.node_path.clone(),
4973        }
4974    }
4975}
4976
4977impl From<&ast::BodyItem> for AstNodeRef {
4978    fn from(value: &ast::BodyItem) -> Self {
4979        match value {
4980            ast::BodyItem::ImportStatement(node) => AstNodeRef {
4981                range: node.into(),
4982                node_path: node.node_path.clone(),
4983            },
4984            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4985                range: node.into(),
4986                node_path: node.node_path.clone(),
4987            },
4988            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4989                range: node.into(),
4990                node_path: node.node_path.clone(),
4991            },
4992            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4993                range: node.into(),
4994                node_path: node.node_path.clone(),
4995            },
4996            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
4997                range: node.into(),
4998                node_path: node.node_path.clone(),
4999            },
5000        }
5001    }
5002}
5003
5004impl From<&ast::Expr> for AstNodeRef {
5005    fn from(value: &ast::Expr) -> Self {
5006        AstNodeRef {
5007            range: SourceRange::from(value),
5008            node_path: value.node_path().cloned(),
5009        }
5010    }
5011}
5012
5013impl From<&AstMutateContext> for AstNodeRef {
5014    fn from(value: &AstMutateContext) -> Self {
5015        AstNodeRef {
5016            range: value.source_range,
5017            node_path: value.node_path.clone(),
5018        }
5019    }
5020}
5021
5022impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5023    type Error = crate::walk::AstNodeError;
5024
5025    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5026        Ok(AstNodeRef {
5027            range: SourceRange::try_from(value)?,
5028            node_path: value.try_into()?,
5029        })
5030    }
5031}
5032
5033impl From<AstNodeRef> for SourceRange {
5034    fn from(value: AstNodeRef) -> Self {
5035        value.range
5036    }
5037}
5038
5039impl Visitor for AstMutateContext {
5040    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5041    type Continue = ();
5042
5043    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5044        filter_and_process(self, node)
5045    }
5046
5047    fn finish(&mut self, node: NodeMut<'_>) {
5048        match &node {
5049            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5050                self.defined_names_stack.pop();
5051            }
5052            _ => {}
5053        }
5054    }
5055}
5056
5057fn filter_and_process(
5058    ctx: &mut AstMutateContext,
5059    node: NodeMut,
5060) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5061    let Ok(node_range) = SourceRange::try_from(&node) else {
5062        // Nodes that can't be converted to a range aren't interesting.
5063        return TraversalReturn::new_continue(());
5064    };
5065    // If we're adding a variable declaration, we need to look at variable
5066    // declaration expressions to see if it already has a variable, before
5067    // continuing. The variable declaration's source range won't match the
5068    // target; its init expression will.
5069    if let NodeMut::VariableDeclaration(var_decl) = &node {
5070        let expr_range = SourceRange::from(&var_decl.declaration.init);
5071        let expr_node_path = var_decl.declaration.init.node_path();
5072        if source_ref_matches(ctx, expr_range, expr_node_path) {
5073            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5074                // We found the variable declaration expression. It doesn't need
5075                // to be added.
5076                return TraversalReturn::new_break(Ok((
5077                    AstNodeRef::from(&**var_decl),
5078                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5079                )));
5080            }
5081            if let AstMutateCommand::DeleteNode = &ctx.command {
5082                // We found the variable declaration. Delete the variable along
5083                // with the segment.
5084                return TraversalReturn {
5085                    mutate_body_item: MutateBodyItem::Delete,
5086                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5087                };
5088            }
5089        }
5090    }
5091    // Similar thing with expression statement. We need to look at the
5092    // expression inside it.
5093    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5094        let expr_range = SourceRange::from(&expr_stmt.expression);
5095        let expr_node_path = expr_stmt.expression.node_path();
5096        if source_ref_matches(ctx, expr_range, expr_node_path) {
5097            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5098                // We found the node wrapped in an expression statement. Process
5099                // the statement.
5100                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5101                    return TraversalReturn::new_continue(());
5102                };
5103                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5104            }
5105            if let AstMutateCommand::DeleteNode = &ctx.command {
5106                // We found the node wrapped in an expression statement. Delete
5107                // the whole statement.
5108                return TraversalReturn {
5109                    mutate_body_item: MutateBodyItem::Delete,
5110                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5111                };
5112            }
5113        }
5114    }
5115
5116    if ctx.command.needs_defined_names_stack() {
5117        if let NodeMut::Program(program) = &node {
5118            ctx.defined_names_stack.push(find_defined_names(*program));
5119        } else if let NodeMut::SketchBlock(block) = &node {
5120            ctx.defined_names_stack.push(find_defined_names(&block.body));
5121        }
5122    }
5123
5124    // Make sure the node matches the source ref.
5125    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5126    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5127        return TraversalReturn::new_continue(());
5128    }
5129    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5130        return TraversalReturn::new_continue(());
5131    };
5132    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5133}
5134
5135fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5136    match &ctx.node_path {
5137        Some(target) => Some(target) == node_path,
5138        None => node_range == ctx.source_range,
5139    }
5140}
5141
5142fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5143    match &ctx.command {
5144        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5145            if let NodeMut::SketchBlock(sketch_block) = node {
5146                sketch_block
5147                    .body
5148                    .items
5149                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5150                        inner: ast::ExpressionStatement {
5151                            expression: expr.clone(),
5152                            digest: None,
5153                        },
5154                        start: Default::default(),
5155                        end: Default::default(),
5156                        module_id: Default::default(),
5157                        node_path: None,
5158                        outer_attrs: Default::default(),
5159                        pre_comments: Default::default(),
5160                        comment_start: Default::default(),
5161                    }));
5162                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5163            }
5164        }
5165        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5166            if let NodeMut::SketchBlock(sketch_block) = node {
5167                let empty_defined_names = HashSet::new();
5168                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5169                let Ok(name) = next_free_name(prefix, defined_names) else {
5170                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5171                };
5172                sketch_block
5173                    .body
5174                    .items
5175                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5176                        ast::VariableDeclaration::new(
5177                            ast::VariableDeclarator::new(&name, expr.clone()),
5178                            ast::ItemVisibility::Default,
5179                            ast::VariableKind::Const,
5180                        ),
5181                    ))));
5182                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5183            }
5184        }
5185        AstMutateCommand::AddVariableDeclaration { prefix } => {
5186            if let NodeMut::VariableDeclaration(inner) = node {
5187                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5188            }
5189            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5190                let empty_defined_names = HashSet::new();
5191                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5192                let Ok(name) = next_free_name(prefix, defined_names) else {
5193                    // TODO: Return an error instead?
5194                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5195                };
5196                let mutate_node =
5197                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5198                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5199                        ast::ItemVisibility::Default,
5200                        ast::VariableKind::Const,
5201                    ))));
5202                return TraversalReturn {
5203                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5204                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5205                };
5206            }
5207        }
5208        AstMutateCommand::EditPoint { at } => {
5209            if let NodeMut::CallExpressionKw(call) = node {
5210                if call.callee.name.name != POINT_FN {
5211                    return TraversalReturn::new_continue(());
5212                }
5213                // Update the arguments.
5214                for labeled_arg in &mut call.arguments {
5215                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5216                        labeled_arg.arg = at.clone();
5217                    }
5218                }
5219                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5220            }
5221        }
5222        AstMutateCommand::EditLine {
5223            start,
5224            end,
5225            construction,
5226        } => {
5227            if let NodeMut::CallExpressionKw(call) = node {
5228                if call.callee.name.name != LINE_FN {
5229                    return TraversalReturn::new_continue(());
5230                }
5231                // Update the arguments.
5232                for labeled_arg in &mut call.arguments {
5233                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5234                        labeled_arg.arg = start.clone();
5235                    }
5236                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5237                        labeled_arg.arg = end.clone();
5238                    }
5239                }
5240                // Handle construction kwarg
5241                if let Some(construction_value) = construction {
5242                    let construction_exists = call
5243                        .arguments
5244                        .iter()
5245                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5246                    if *construction_value {
5247                        // Add or update construction=true
5248                        if construction_exists {
5249                            // Update existing construction kwarg
5250                            for labeled_arg in &mut call.arguments {
5251                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5252                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5253                                        value: ast::LiteralValue::Bool(true),
5254                                        raw: "true".to_string(),
5255                                        digest: None,
5256                                    })));
5257                                }
5258                            }
5259                        } else {
5260                            // Add new construction kwarg
5261                            call.arguments.push(ast::LabeledArg {
5262                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5263                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5264                                    value: ast::LiteralValue::Bool(true),
5265                                    raw: "true".to_string(),
5266                                    digest: None,
5267                                }))),
5268                            });
5269                        }
5270                    } else {
5271                        // Remove construction kwarg if it exists
5272                        call.arguments
5273                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5274                    }
5275                }
5276                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5277            }
5278        }
5279        AstMutateCommand::EditArc {
5280            start,
5281            end,
5282            center,
5283            construction,
5284        } => {
5285            if let NodeMut::CallExpressionKw(call) = node {
5286                if call.callee.name.name != ARC_FN {
5287                    return TraversalReturn::new_continue(());
5288                }
5289                // Update the arguments.
5290                for labeled_arg in &mut call.arguments {
5291                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5292                        labeled_arg.arg = start.clone();
5293                    }
5294                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5295                        labeled_arg.arg = end.clone();
5296                    }
5297                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5298                        labeled_arg.arg = center.clone();
5299                    }
5300                }
5301                // Handle construction kwarg
5302                if let Some(construction_value) = construction {
5303                    let construction_exists = call
5304                        .arguments
5305                        .iter()
5306                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5307                    if *construction_value {
5308                        // Add or update construction=true
5309                        if construction_exists {
5310                            // Update existing construction kwarg
5311                            for labeled_arg in &mut call.arguments {
5312                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5313                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5314                                        value: ast::LiteralValue::Bool(true),
5315                                        raw: "true".to_string(),
5316                                        digest: None,
5317                                    })));
5318                                }
5319                            }
5320                        } else {
5321                            // Add new construction kwarg
5322                            call.arguments.push(ast::LabeledArg {
5323                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5324                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5325                                    value: ast::LiteralValue::Bool(true),
5326                                    raw: "true".to_string(),
5327                                    digest: None,
5328                                }))),
5329                            });
5330                        }
5331                    } else {
5332                        // Remove construction kwarg if it exists
5333                        call.arguments
5334                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5335                    }
5336                }
5337                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5338            }
5339        }
5340        AstMutateCommand::EditCircle {
5341            start,
5342            center,
5343            construction,
5344        } => {
5345            if let NodeMut::CallExpressionKw(call) = node {
5346                if call.callee.name.name != CIRCLE_FN {
5347                    return TraversalReturn::new_continue(());
5348                }
5349                // Update the arguments.
5350                for labeled_arg in &mut call.arguments {
5351                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5352                        labeled_arg.arg = start.clone();
5353                    }
5354                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5355                        labeled_arg.arg = center.clone();
5356                    }
5357                }
5358                // Handle construction kwarg
5359                if let Some(construction_value) = construction {
5360                    let construction_exists = call
5361                        .arguments
5362                        .iter()
5363                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5364                    if *construction_value {
5365                        if construction_exists {
5366                            for labeled_arg in &mut call.arguments {
5367                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5368                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5369                                        value: ast::LiteralValue::Bool(true),
5370                                        raw: "true".to_string(),
5371                                        digest: None,
5372                                    })));
5373                                }
5374                            }
5375                        } else {
5376                            call.arguments.push(ast::LabeledArg {
5377                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5378                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5379                                    value: ast::LiteralValue::Bool(true),
5380                                    raw: "true".to_string(),
5381                                    digest: None,
5382                                }))),
5383                            });
5384                        }
5385                    } else {
5386                        call.arguments
5387                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5388                    }
5389                }
5390                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5391            }
5392        }
5393        AstMutateCommand::EditConstraintValue { value } => {
5394            if let NodeMut::BinaryExpression(binary_expr) = node {
5395                let left_is_constraint = matches!(
5396                    &binary_expr.left,
5397                    ast::BinaryPart::CallExpressionKw(call)
5398                        if matches!(
5399                            call.callee.name.name.as_str(),
5400                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5401                        )
5402                );
5403                if left_is_constraint {
5404                    binary_expr.right = value.clone();
5405                } else {
5406                    binary_expr.left = value.clone();
5407                }
5408
5409                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5410            }
5411        }
5412        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5413            if let NodeMut::BinaryExpression(binary_expr) = node {
5414                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5415                    return TraversalReturn::new_continue(());
5416                };
5417                if !matches!(
5418                    call.callee.name.name.as_str(),
5419                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5420                ) {
5421                    return TraversalReturn::new_continue(());
5422                }
5423
5424                if let Some(label_arg) = call
5425                    .arguments
5426                    .iter_mut()
5427                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5428                {
5429                    label_arg.arg = label_position.clone();
5430                } else {
5431                    call.arguments.push(ast::LabeledArg {
5432                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5433                        arg: label_position.clone(),
5434                    });
5435                }
5436
5437                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5438            }
5439        }
5440        AstMutateCommand::EditCallUnlabeled { arg } => {
5441            if let NodeMut::CallExpressionKw(call) = node {
5442                call.unlabeled = Some(arg.clone());
5443                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5444            }
5445        }
5446        #[cfg(feature = "artifact-graph")]
5447        AstMutateCommand::EditVarInitialValue { value } => {
5448            if let NodeMut::NumericLiteral(numeric_literal) = node {
5449                // Update the initial value.
5450                let Ok(literal) = to_source_number(*value) else {
5451                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5452                        "Could not convert number to AST literal: {:?}",
5453                        *value
5454                    ))));
5455                };
5456                *numeric_literal = ast::Node::no_src(literal);
5457                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5458            }
5459        }
5460        AstMutateCommand::DeleteNode => {
5461            return TraversalReturn {
5462                mutate_body_item: MutateBodyItem::Delete,
5463                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5464            };
5465        }
5466    }
5467    TraversalReturn::new_continue(())
5468}
5469
5470struct FindSketchBlockSourceRange {
5471    /// The source range of the sketch block before mutation.
5472    target_before_mutation: SourceRange,
5473    /// The source range of the sketch block's last body item after mutation. We
5474    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5475    /// shared reference.
5476    found: Cell<Option<AstNodeRef>>,
5477}
5478
5479impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5480    type Error = crate::front::Error;
5481
5482    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5483        let Ok(node_range) = SourceRange::try_from(&node) else {
5484            return Ok(true);
5485        };
5486
5487        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5488            if node_range.module_id() == self.target_before_mutation.module_id()
5489                && node_range.start() == self.target_before_mutation.start()
5490                // End shouldn't match since we added something.
5491                && node_range.end() >= self.target_before_mutation.end()
5492            {
5493                self.found.set(sketch_block.body.items.last().map(|item| match item {
5494                    // For declarations like `circle1 = circle(...)`, use
5495                    // the init expression range so lookup in source_range_to_object
5496                    // matches the segment source range.
5497                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5498                    _ => AstNodeRef::from(item),
5499                }));
5500                return Ok(false);
5501            } else {
5502                // We found a different sketch block. No need to descend into
5503                // its children since sketch blocks cannot be nested.
5504                return Ok(true);
5505            }
5506        }
5507
5508        for child in node.children().iter() {
5509            if !child.visit(*self)? {
5510                return Ok(false);
5511            }
5512        }
5513
5514        Ok(true)
5515    }
5516}
5517
5518struct FindSketchBlockByNodePath {
5519    /// The Node Path of the sketch block before mutation.
5520    target_node_path: ast::NodePath,
5521    /// The ref of the sketch block's last body item after mutation. We need to
5522    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5523    /// reference.
5524    found: Cell<Option<AstNodeRef>>,
5525}
5526
5527impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5528    type Error = crate::front::Error;
5529
5530    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5531        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5532            return Ok(true);
5533        };
5534
5535        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5536            if let Some(node_path) = node_path
5537                && node_path == self.target_node_path
5538            {
5539                self.found.set(sketch_block.body.items.last().map(|item| match item {
5540                    // For declarations like `circle1 = circle(...)`, use
5541                    // the init expression range so lookup in source_range_to_object
5542                    // matches the segment source range.
5543                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5544                    _ => AstNodeRef::from(item),
5545                }));
5546
5547                return Ok(false);
5548            } else {
5549                // We found a different sketch block. No need to descend into
5550                // its children since sketch blocks cannot be nested.
5551                return Ok(true);
5552            }
5553        }
5554
5555        for child in node.children().iter() {
5556            if !child.visit(*self)? {
5557                return Ok(false);
5558            }
5559        }
5560
5561        Ok(true)
5562    }
5563}
5564
5565/// After adding an item to a sketch block, find the sketch block, and get the
5566/// source range of the added item. We assume that the added item is the last
5567/// item in the sketch block and that the sketch block's source range has grown,
5568/// but not moved from its starting offset.
5569///
5570/// TODO: Do we need to format *before* mutation in case formatting moves the
5571/// sketch block forward?
5572fn find_sketch_block_added_item(
5573    ast: &ast::Node<ast::Program>,
5574    sketch_block_before_mutation: &AstNodeRef,
5575) -> Result<AstNodeRef, KclError> {
5576    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5577        let find = FindSketchBlockByNodePath {
5578            target_node_path: node_path.clone(),
5579            found: Cell::new(None),
5580        };
5581        let node = crate::walk::Node::from(ast);
5582        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5583        find.found.into_inner().ok_or_else(|| {
5584            KclError::refactor(format!(
5585                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5586            ))
5587        })
5588    } else {
5589        // No NodePath. Fall back to legacy source range.
5590        let find = FindSketchBlockSourceRange {
5591            target_before_mutation: sketch_block_before_mutation.range,
5592            found: Cell::new(None),
5593        };
5594        let node = crate::walk::Node::from(ast);
5595        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5596        find.found.into_inner().ok_or_else(|| KclError::refactor(
5597            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?"),
5598        ))
5599    }
5600}
5601
5602fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5603    // TODO: Don't duplicate this from lib.rs Program.
5604    ast.recast_top(&Default::default(), 0)
5605}
5606
5607pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5608    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5609        inner: ast::ArrayExpression {
5610            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5611            non_code_meta: Default::default(),
5612            digest: None,
5613        },
5614        start: Default::default(),
5615        end: Default::default(),
5616        module_id: Default::default(),
5617        node_path: None,
5618        outer_attrs: Default::default(),
5619        pre_comments: Default::default(),
5620        comment_start: Default::default(),
5621    })))
5622}
5623
5624fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5625    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5626        ast::ArrayExpression {
5627            elements: vec![
5628                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5629                    point.x,
5630                )?)))),
5631                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5632                    point.y,
5633                )?)))),
5634            ],
5635            non_code_meta: Default::default(),
5636            digest: None,
5637        },
5638    ))))
5639}
5640
5641fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5642    match expr {
5643        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5644            inner: ast::Literal::from(to_source_number(*number)?),
5645            start: Default::default(),
5646            end: Default::default(),
5647            module_id: Default::default(),
5648            node_path: None,
5649            outer_attrs: Default::default(),
5650            pre_comments: Default::default(),
5651            comment_start: Default::default(),
5652        }))),
5653        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5654            inner: ast::SketchVar {
5655                initial: Some(Box::new(ast::Node {
5656                    inner: to_source_number(*number)?,
5657                    start: Default::default(),
5658                    end: Default::default(),
5659                    module_id: Default::default(),
5660                    node_path: None,
5661                    outer_attrs: Default::default(),
5662                    pre_comments: Default::default(),
5663                    comment_start: Default::default(),
5664                })),
5665                digest: None,
5666            },
5667            start: Default::default(),
5668            end: Default::default(),
5669            module_id: Default::default(),
5670            node_path: None,
5671            outer_attrs: Default::default(),
5672            pre_comments: Default::default(),
5673            comment_start: Default::default(),
5674        }))),
5675        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5676    }
5677}
5678
5679fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5680    Ok(ast::NumericLiteral {
5681        value: number.value,
5682        suffix: number.units,
5683        raw: format_number_literal(number.value, number.units, None)?,
5684        digest: None,
5685    })
5686}
5687
5688pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5689    ast::Expr::Name(Box::new(ast_name(name)))
5690}
5691
5692fn ast_name(name: String) -> ast::Node<ast::Name> {
5693    ast::Node {
5694        inner: ast::Name {
5695            name: ast::Node {
5696                inner: ast::Identifier { name, digest: None },
5697                start: Default::default(),
5698                end: Default::default(),
5699                module_id: Default::default(),
5700                node_path: None,
5701                outer_attrs: Default::default(),
5702                pre_comments: Default::default(),
5703                comment_start: Default::default(),
5704            },
5705            path: Vec::new(),
5706            abs_path: false,
5707            digest: None,
5708        },
5709        start: Default::default(),
5710        end: Default::default(),
5711        module_id: Default::default(),
5712        node_path: None,
5713        outer_attrs: Default::default(),
5714        pre_comments: Default::default(),
5715        comment_start: Default::default(),
5716    }
5717}
5718
5719pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5720    ast::Name {
5721        name: ast::Node {
5722            inner: ast::Identifier {
5723                name: name.to_owned(),
5724                digest: None,
5725            },
5726            start: Default::default(),
5727            end: Default::default(),
5728            module_id: Default::default(),
5729            node_path: None,
5730            outer_attrs: Default::default(),
5731            pre_comments: Default::default(),
5732            comment_start: Default::default(),
5733        },
5734        path: Default::default(),
5735        abs_path: false,
5736        digest: None,
5737    }
5738}
5739
5740// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
5741
5742/// Create an AST node for coincident([expr1, expr2, ...])
5743pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5744    let elements = exprs.into_iter().collect::<Vec<_>>();
5745    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5746
5747    // Create array [expr1, expr2, ...]
5748    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5749        elements,
5750        digest: None,
5751        non_code_meta: Default::default(),
5752    })));
5753
5754    // Create coincident([...])
5755    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5756        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5757        unlabeled: Some(array_expr),
5758        arguments: Default::default(),
5759        digest: None,
5760        non_code_meta: Default::default(),
5761    })))
5762}
5763
5764/// Create an AST node for line(start = [...], end = [...])
5765pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5766    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5767        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5768        unlabeled: None,
5769        arguments: vec![
5770            ast::LabeledArg {
5771                label: Some(ast::Identifier::new(LINE_START_PARAM)),
5772                arg: start_ast,
5773            },
5774            ast::LabeledArg {
5775                label: Some(ast::Identifier::new(LINE_END_PARAM)),
5776                arg: end_ast,
5777            },
5778        ],
5779        digest: None,
5780        non_code_meta: Default::default(),
5781    })))
5782}
5783
5784/// Create an AST node for arc(start = [...], end = [...], center = [...])
5785pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5786    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5787        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5788        unlabeled: None,
5789        arguments: vec![
5790            ast::LabeledArg {
5791                label: Some(ast::Identifier::new(ARC_START_PARAM)),
5792                arg: start_ast,
5793            },
5794            ast::LabeledArg {
5795                label: Some(ast::Identifier::new(ARC_END_PARAM)),
5796                arg: end_ast,
5797            },
5798            ast::LabeledArg {
5799                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5800                arg: center_ast,
5801            },
5802        ],
5803        digest: None,
5804        non_code_meta: Default::default(),
5805    })))
5806}
5807
5808/// Create an AST node for circle(start = [...], center = [...])
5809pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5810    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5811        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5812        unlabeled: None,
5813        arguments: vec![
5814            ast::LabeledArg {
5815                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5816                arg: start_ast,
5817            },
5818            ast::LabeledArg {
5819                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5820                arg: center_ast,
5821            },
5822        ],
5823        digest: None,
5824        non_code_meta: Default::default(),
5825    })))
5826}
5827
5828/// Create an AST node for horizontal(line)
5829pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5830    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5831        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5832        unlabeled: Some(line_expr),
5833        arguments: Default::default(),
5834        digest: None,
5835        non_code_meta: Default::default(),
5836    })))
5837}
5838
5839/// Create an AST node for vertical(line)
5840pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5841    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5842        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5843        unlabeled: Some(line_expr),
5844        arguments: Default::default(),
5845        digest: None,
5846        non_code_meta: Default::default(),
5847    })))
5848}
5849
5850/// Create a member expression like object.property (e.g., line1.end)
5851pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5852    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5853        object: object_expr,
5854        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5855            name: ast::Node::no_src(ast::Identifier {
5856                name: property.to_string(),
5857                digest: None,
5858            }),
5859            path: Vec::new(),
5860            abs_path: false,
5861            digest: None,
5862        }))),
5863        computed: false,
5864        digest: None,
5865    })))
5866}
5867
5868/// Create an AST node for `fixed([point, [x, y]])`.
5869fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5870    // Create [x, y] array literal.
5871    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5872        position.x,
5873    )?))));
5874    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5875        position.y,
5876    )?))));
5877    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5878        elements: vec![x_literal, y_literal],
5879        digest: None,
5880        non_code_meta: Default::default(),
5881    })));
5882
5883    // Create [point, [x, y]] outer array.
5884    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5885        elements: vec![point_expr, point_array],
5886        digest: None,
5887        non_code_meta: Default::default(),
5888    })));
5889
5890    // Create fixed([...])
5891    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5892        ast::CallExpressionKw {
5893            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5894            unlabeled: Some(array_expr),
5895            arguments: Default::default(),
5896            digest: None,
5897            non_code_meta: Default::default(),
5898        },
5899    ))))
5900}
5901
5902/// Create an AST node for equalLength([line1, line2, ...])
5903pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5904    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5905        elements: line_exprs,
5906        digest: None,
5907        non_code_meta: Default::default(),
5908    })));
5909
5910    // Create equalLength([...])
5911    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5912        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5913        unlabeled: Some(array_expr),
5914        arguments: Default::default(),
5915        digest: None,
5916        non_code_meta: Default::default(),
5917    })))
5918}
5919
5920/// Create an AST node for equalRadius([seg1, seg2, ...])
5921pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5922    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5923        elements: segment_exprs,
5924        digest: None,
5925        non_code_meta: Default::default(),
5926    })));
5927
5928    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5929        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5930        unlabeled: Some(array_expr),
5931        arguments: Default::default(),
5932        digest: None,
5933        non_code_meta: Default::default(),
5934    })))
5935}
5936
5937/// Create an AST node for tangent([seg1, seg2])
5938pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5939    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5940        elements: vec![seg1_expr, seg2_expr],
5941        digest: None,
5942        non_code_meta: Default::default(),
5943    })));
5944
5945    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5946        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5947        unlabeled: Some(array_expr),
5948        arguments: Default::default(),
5949        digest: None,
5950        non_code_meta: Default::default(),
5951    })))
5952}
5953
5954/// Create an AST node for symmetric([input1, input2], axis = line)
5955pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5956    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5957        elements: input_exprs,
5958        digest: None,
5959        non_code_meta: Default::default(),
5960    })));
5961    let arguments = vec![ast::LabeledArg {
5962        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5963        arg: axis_expr,
5964    }];
5965
5966    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5967        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5968        unlabeled: Some(array_expr),
5969        arguments,
5970        digest: None,
5971        non_code_meta: Default::default(),
5972    })))
5973}
5974
5975/// Create an AST node for midpoint(segment, point = point)
5976pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5977    let arguments = vec![ast::LabeledArg {
5978        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5979        arg: point_expr,
5980    }];
5981
5982    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5983        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5984        unlabeled: Some(segment_expr),
5985        arguments,
5986        digest: None,
5987        non_code_meta: Default::default(),
5988    })))
5989}
5990
5991#[cfg(all(feature = "artifact-graph", test))]
5992mod tests {
5993    use super::*;
5994    use crate::engine::PlaneName;
5995    use crate::execution::cache::SketchModeState;
5996    use crate::execution::cache::clear_mem_cache;
5997    use crate::execution::cache::read_old_memory;
5998    use crate::execution::cache::write_old_memory;
5999    use crate::front::Distance;
6000    use crate::front::Fixed;
6001    use crate::front::FixedPoint;
6002    use crate::front::Midpoint;
6003    use crate::front::Object;
6004    use crate::front::Plane;
6005    use crate::front::Sketch;
6006    use crate::front::Tangent;
6007    use crate::frontend::sketch::Vertical;
6008    use crate::pretty::NumericSuffix;
6009
6010    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6011        for object in &scene_graph.objects {
6012            if let ObjectKind::Sketch(_) = &object.kind {
6013                return Some(object);
6014            }
6015        }
6016        None
6017    }
6018
6019    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6020        for object in &scene_graph.objects {
6021            if let ObjectKind::Face(_) = &object.kind {
6022                return Some(object);
6023            }
6024        }
6025        None
6026    }
6027
6028    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6029        for object in &scene_graph.objects {
6030            if matches!(&object.kind, ObjectKind::Wall(_)) {
6031                return Some(object.id);
6032            }
6033        }
6034        None
6035    }
6036
6037    #[test]
6038    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6039        let source = "\
6040region001 = region(point = [0.1, 0.1], sketch = s)
6041extrude001 = extrude(region001, length = 5)
6042revolve001 = revolve(region001, axis = Y)
6043sweep001 = sweep(region001, path = path001)
6044loft001 = loft(region001)
6045not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6046";
6047
6048        let program = Program::parse(source).unwrap().0.unwrap();
6049
6050        assert_eq!(
6051            region_name_from_sweep_variable(&program.ast, "extrude001"),
6052            Some("region001".to_owned())
6053        );
6054        assert_eq!(
6055            region_name_from_sweep_variable(&program.ast, "revolve001"),
6056            Some("region001".to_owned())
6057        );
6058        assert_eq!(
6059            region_name_from_sweep_variable(&program.ast, "sweep001"),
6060            Some("region001".to_owned())
6061        );
6062        assert_eq!(
6063            region_name_from_sweep_variable(&program.ast, "loft001"),
6064            Some("region001".to_owned())
6065        );
6066        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6067    }
6068
6069    #[track_caller]
6070    fn expect_sketch(object: &Object) -> &Sketch {
6071        if let ObjectKind::Sketch(sketch) = &object.kind {
6072            sketch
6073        } else {
6074            panic!("Object is not a sketch: {:?}", object);
6075        }
6076    }
6077
6078    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6079        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6080        let ObjectKind::Segment {
6081            segment: Segment::Point(point),
6082        } = &point_object.kind
6083        else {
6084            panic!("Object is not a point segment: {point_object:?}");
6085        };
6086        point.position.clone()
6087    }
6088
6089    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6090        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6091        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6092    }
6093
6094    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6095        LineCtor {
6096            start: Point2d {
6097                x: Expr::Number(Number { value: start_x, units }),
6098                y: Expr::Number(Number { value: start_y, units }),
6099            },
6100            end: Point2d {
6101                x: Expr::Number(Number { value: end_x, units }),
6102                y: Expr::Number(Number { value: end_y, units }),
6103            },
6104            construction: None,
6105        }
6106    }
6107
6108    async fn create_sketch_with_single_line(
6109        frontend: &mut FrontendState,
6110        ctx: &ExecutorContext,
6111        mock_ctx: &ExecutorContext,
6112        version: Version,
6113    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6114        frontend.program = Program::empty();
6115
6116        let sketch_args = SketchCtor {
6117            on: Plane::Default(PlaneName::Xy),
6118        };
6119        let (_src_delta, _scene_delta, sketch_id) = frontend
6120            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6121            .await
6122            .unwrap();
6123
6124        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6125        let (source_delta, scene_graph_delta) = frontend
6126            .add_segment(mock_ctx, version, sketch_id, segment, None)
6127            .await
6128            .unwrap();
6129        let line_id = *scene_graph_delta
6130            .new_objects
6131            .last()
6132            .expect("Expected line object id to be created");
6133
6134        (sketch_id, line_id, source_delta, scene_graph_delta)
6135    }
6136
6137    #[tokio::test(flavor = "multi_thread")]
6138    async fn test_sketch_checkpoint_round_trip_restores_state() {
6139        let mut frontend = FrontendState::new();
6140        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6141        let mock_ctx = ExecutorContext::new_mock(None).await;
6142        let version = Version(0);
6143
6144        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6145            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6146
6147        let expected_source = source_delta.text.clone();
6148        let expected_scene_graph = frontend.scene_graph.clone();
6149        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6150        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6151
6152        let checkpoint_id = frontend
6153            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6154            .await
6155            .unwrap();
6156
6157        let edited_segments = vec![ExistingSegmentCtor {
6158            id: line_id,
6159            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6160        }];
6161        let (edited_source, _edited_scene) = frontend
6162            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6163            .await
6164            .unwrap();
6165        assert_ne!(edited_source.text, expected_source);
6166
6167        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6168
6169        assert_eq!(restored.source_delta.text, expected_source);
6170        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6171        assert!(restored.scene_graph_delta.invalidates_ids);
6172        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6173        assert_eq!(frontend.scene_graph, expected_scene_graph);
6174        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6175
6176        ctx.close().await;
6177        mock_ctx.close().await;
6178    }
6179
6180    #[tokio::test(flavor = "multi_thread")]
6181    async fn test_sketch_checkpoints_prune_oldest_entries() {
6182        let mut frontend = FrontendState::new();
6183        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6184        let mock_ctx = ExecutorContext::new_mock(None).await;
6185        let version = Version(0);
6186
6187        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6188            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6189
6190        let mut checkpoint_ids = Vec::new();
6191        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6192            checkpoint_ids.push(
6193                frontend
6194                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6195                    .await
6196                    .unwrap(),
6197            );
6198        }
6199
6200        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6201        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6202
6203        let oldest_retained = checkpoint_ids[3];
6204        assert_eq!(
6205            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6206            Some(oldest_retained)
6207        );
6208
6209        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6210        assert!(evicted_restore.is_err());
6211        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6212
6213        frontend
6214            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6215            .await
6216            .unwrap();
6217
6218        ctx.close().await;
6219        mock_ctx.close().await;
6220    }
6221
6222    #[tokio::test(flavor = "multi_thread")]
6223    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6224        let mut frontend = FrontendState::new();
6225        let missing_checkpoint = SketchCheckpointId::new(999);
6226
6227        let err = frontend
6228            .restore_sketch_checkpoint(missing_checkpoint)
6229            .await
6230            .expect_err("Expected restore to fail for missing checkpoint");
6231
6232        assert!(err.msg.contains("Sketch checkpoint not found"));
6233    }
6234
6235    #[tokio::test(flavor = "multi_thread")]
6236    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6237        let mut frontend = FrontendState::new();
6238        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6239        let mock_ctx = ExecutorContext::new_mock(None).await;
6240        let version = Version(0);
6241
6242        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6243            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6244
6245        let checkpoint_a = frontend
6246            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6247            .await
6248            .unwrap();
6249        let checkpoint_b = frontend
6250            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6251            .await
6252            .unwrap();
6253        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6254
6255        frontend.clear_sketch_checkpoints();
6256        assert!(frontend.sketch_checkpoints.is_empty());
6257        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6258        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6259
6260        ctx.close().await;
6261        mock_ctx.close().await;
6262    }
6263
6264    #[tokio::test(flavor = "multi_thread")]
6265    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6266        let mut frontend = FrontendState::new();
6267        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6268        let mock_ctx = ExecutorContext::new_mock(None).await;
6269        let version = Version(0);
6270
6271        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6272            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6273        let old_source = source_delta.text.clone();
6274        let old_checkpoint = frontend
6275            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6276            .await
6277            .unwrap();
6278        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6279
6280        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6281            .unwrap()
6282            .0
6283            .unwrap();
6284
6285        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6286        let SetProgramOutcome::Success {
6287            checkpoint_id: Some(new_checkpoint),
6288            ..
6289        } = result
6290        else {
6291            panic!("Expected Success with a fresh checkpoint baseline");
6292        };
6293
6294        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6295
6296        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6297        assert_eq!(old_restore.source_delta.text, old_source);
6298
6299        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6300        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6301
6302        ctx.close().await;
6303        mock_ctx.close().await;
6304    }
6305
6306    #[tokio::test(flavor = "multi_thread")]
6307    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6308        let mut frontend = FrontendState::new();
6309        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6310        let mock_ctx = ExecutorContext::new_mock(None).await;
6311        let version = Version(0);
6312
6313        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6314            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6315        let old_checkpoint = frontend
6316            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6317            .await
6318            .unwrap();
6319        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6320
6321        let failing_program = Program::parse(
6322            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6323        )
6324        .unwrap()
6325        .0
6326        .unwrap();
6327
6328        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6329        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6330        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6331        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6332
6333        ctx.close().await;
6334        mock_ctx.close().await;
6335    }
6336
6337    #[tokio::test(flavor = "multi_thread")]
6338    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6339        let mut frontend = FrontendState::new();
6340        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6341
6342        let program = Program::parse(
6343            "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",
6344        )
6345        .unwrap()
6346        .0
6347        .unwrap();
6348        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6349        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6350            panic!("Expected successful baseline program execution");
6351        };
6352
6353        clear_mem_cache().await;
6354        assert!(read_old_memory().await.is_none());
6355
6356        let checkpoint_without_mock_memory = frontend
6357            .create_sketch_checkpoint((*exec_outcome).clone())
6358            .await
6359            .unwrap();
6360
6361        write_old_memory(SketchModeState::new_for_tests()).await;
6362        assert!(read_old_memory().await.is_some());
6363
6364        let checkpoint_with_mock_memory = frontend
6365            .create_sketch_checkpoint((*exec_outcome).clone())
6366            .await
6367            .unwrap();
6368
6369        clear_mem_cache().await;
6370        assert!(read_old_memory().await.is_none());
6371
6372        frontend
6373            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6374            .await
6375            .unwrap();
6376        assert!(read_old_memory().await.is_some());
6377
6378        frontend
6379            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6380            .await
6381            .unwrap();
6382        assert!(read_old_memory().await.is_none());
6383
6384        ctx.close().await;
6385    }
6386
6387    #[tokio::test(flavor = "multi_thread")]
6388    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6389        let source = "\
6390sketch(on = XY) {
6391  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6392}
6393
6394bad = missing_name
6395";
6396        let program = Program::parse(source).unwrap().0.unwrap();
6397
6398        let mut frontend = FrontendState::new();
6399
6400        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6401        let mock_ctx = ExecutorContext::new_mock(None).await;
6402        let version = Version(0);
6403        let project_id = ProjectId(0);
6404        let file_id = FileId(0);
6405
6406        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6407            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6408        };
6409
6410        let sketch_id = frontend
6411            .scene_graph
6412            .objects
6413            .iter()
6414            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6415            .expect("Expected sketch object from errored hack_set_program");
6416
6417        frontend
6418            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6419            .await
6420            .unwrap();
6421
6422        ctx.close().await;
6423        mock_ctx.close().await;
6424    }
6425
6426    #[tokio::test(flavor = "multi_thread")]
6427    async fn test_new_sketch_add_point_edit_point() {
6428        let program = Program::empty();
6429
6430        let mut frontend = FrontendState::new();
6431        frontend.program = program;
6432
6433        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6434        let mock_ctx = ExecutorContext::new_mock(None).await;
6435        let version = Version(0);
6436
6437        let sketch_args = SketchCtor {
6438            on: Plane::Default(PlaneName::Xy),
6439        };
6440        let (_src_delta, scene_delta, sketch_id) = frontend
6441            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6442            .await
6443            .unwrap();
6444        assert_eq!(sketch_id, ObjectId(1));
6445        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6446        let sketch_object = &scene_delta.new_graph.objects[1];
6447        assert_eq!(sketch_object.id, ObjectId(1));
6448        assert_eq!(
6449            sketch_object.kind,
6450            ObjectKind::Sketch(Sketch {
6451                args: SketchCtor {
6452                    on: Plane::Default(PlaneName::Xy)
6453                },
6454                plane: ObjectId(0),
6455                segments: vec![],
6456                constraints: vec![],
6457            })
6458        );
6459        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6460
6461        let point_ctor = PointCtor {
6462            position: Point2d {
6463                x: Expr::Number(Number {
6464                    value: 1.0,
6465                    units: NumericSuffix::Inch,
6466                }),
6467                y: Expr::Number(Number {
6468                    value: 2.0,
6469                    units: NumericSuffix::Inch,
6470                }),
6471            },
6472        };
6473        let segment = SegmentCtor::Point(point_ctor);
6474        let (src_delta, scene_delta) = frontend
6475            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6476            .await
6477            .unwrap();
6478        assert_eq!(
6479            src_delta.text.as_str(),
6480            "sketch001 = sketch(on = XY) {
6481  point(at = [1in, 2in])
6482}
6483"
6484        );
6485        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6486        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6487        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6488            assert_eq!(scene_object.id.0, i);
6489        }
6490
6491        let point_id = *scene_delta.new_objects.last().unwrap();
6492
6493        let point_ctor = PointCtor {
6494            position: Point2d {
6495                x: Expr::Number(Number {
6496                    value: 3.0,
6497                    units: NumericSuffix::Inch,
6498                }),
6499                y: Expr::Number(Number {
6500                    value: 4.0,
6501                    units: NumericSuffix::Inch,
6502                }),
6503            },
6504        };
6505        let segments = vec![ExistingSegmentCtor {
6506            id: point_id,
6507            ctor: SegmentCtor::Point(point_ctor),
6508        }];
6509        let (src_delta, scene_delta) = frontend
6510            .edit_segments(&mock_ctx, version, sketch_id, segments)
6511            .await
6512            .unwrap();
6513        assert_eq!(
6514            src_delta.text.as_str(),
6515            "sketch001 = sketch(on = XY) {
6516  point(at = [3in, 4in])
6517}
6518"
6519        );
6520        assert_eq!(scene_delta.new_objects, vec![]);
6521        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6522
6523        ctx.close().await;
6524        mock_ctx.close().await;
6525    }
6526
6527    #[tokio::test(flavor = "multi_thread")]
6528    async fn test_new_sketch_add_line_edit_line() {
6529        let program = Program::empty();
6530
6531        let mut frontend = FrontendState::new();
6532        frontend.program = program;
6533
6534        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6535        let mock_ctx = ExecutorContext::new_mock(None).await;
6536        let version = Version(0);
6537
6538        let sketch_args = SketchCtor {
6539            on: Plane::Default(PlaneName::Xy),
6540        };
6541        let (_src_delta, scene_delta, sketch_id) = frontend
6542            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6543            .await
6544            .unwrap();
6545        assert_eq!(sketch_id, ObjectId(1));
6546        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6547        let sketch_object = &scene_delta.new_graph.objects[1];
6548        assert_eq!(sketch_object.id, ObjectId(1));
6549        assert_eq!(
6550            sketch_object.kind,
6551            ObjectKind::Sketch(Sketch {
6552                args: SketchCtor {
6553                    on: Plane::Default(PlaneName::Xy)
6554                },
6555                plane: ObjectId(0),
6556                segments: vec![],
6557                constraints: vec![],
6558            })
6559        );
6560        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6561
6562        let line_ctor = LineCtor {
6563            start: Point2d {
6564                x: Expr::Number(Number {
6565                    value: 0.0,
6566                    units: NumericSuffix::Mm,
6567                }),
6568                y: Expr::Number(Number {
6569                    value: 0.0,
6570                    units: NumericSuffix::Mm,
6571                }),
6572            },
6573            end: Point2d {
6574                x: Expr::Number(Number {
6575                    value: 10.0,
6576                    units: NumericSuffix::Mm,
6577                }),
6578                y: Expr::Number(Number {
6579                    value: 10.0,
6580                    units: NumericSuffix::Mm,
6581                }),
6582            },
6583            construction: None,
6584        };
6585        let segment = SegmentCtor::Line(line_ctor);
6586        let (src_delta, scene_delta) = frontend
6587            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6588            .await
6589            .unwrap();
6590        assert_eq!(
6591            src_delta.text.as_str(),
6592            "sketch001 = sketch(on = XY) {
6593  line(start = [0mm, 0mm], end = [10mm, 10mm])
6594}
6595"
6596        );
6597        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6598        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6599        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6600            assert_eq!(scene_object.id.0, i);
6601        }
6602
6603        // The new objects are the end points and then the line.
6604        let line = *scene_delta.new_objects.last().unwrap();
6605
6606        let line_ctor = LineCtor {
6607            start: Point2d {
6608                x: Expr::Number(Number {
6609                    value: 1.0,
6610                    units: NumericSuffix::Mm,
6611                }),
6612                y: Expr::Number(Number {
6613                    value: 2.0,
6614                    units: NumericSuffix::Mm,
6615                }),
6616            },
6617            end: Point2d {
6618                x: Expr::Number(Number {
6619                    value: 13.0,
6620                    units: NumericSuffix::Mm,
6621                }),
6622                y: Expr::Number(Number {
6623                    value: 14.0,
6624                    units: NumericSuffix::Mm,
6625                }),
6626            },
6627            construction: None,
6628        };
6629        let segments = vec![ExistingSegmentCtor {
6630            id: line,
6631            ctor: SegmentCtor::Line(line_ctor),
6632        }];
6633        let (src_delta, scene_delta) = frontend
6634            .edit_segments(&mock_ctx, version, sketch_id, segments)
6635            .await
6636            .unwrap();
6637        assert_eq!(
6638            src_delta.text.as_str(),
6639            "sketch001 = sketch(on = XY) {
6640  line(start = [1mm, 2mm], end = [13mm, 14mm])
6641}
6642"
6643        );
6644        assert_eq!(scene_delta.new_objects, vec![]);
6645        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6646
6647        ctx.close().await;
6648        mock_ctx.close().await;
6649    }
6650
6651    #[tokio::test(flavor = "multi_thread")]
6652    async fn test_new_sketch_add_arc_edit_arc() {
6653        let program = Program::empty();
6654
6655        let mut frontend = FrontendState::new();
6656        frontend.program = program;
6657
6658        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6659        let mock_ctx = ExecutorContext::new_mock(None).await;
6660        let version = Version(0);
6661
6662        let sketch_args = SketchCtor {
6663            on: Plane::Default(PlaneName::Xy),
6664        };
6665        let (_src_delta, scene_delta, sketch_id) = frontend
6666            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6667            .await
6668            .unwrap();
6669        assert_eq!(sketch_id, ObjectId(1));
6670        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6671        let sketch_object = &scene_delta.new_graph.objects[1];
6672        assert_eq!(sketch_object.id, ObjectId(1));
6673        assert_eq!(
6674            sketch_object.kind,
6675            ObjectKind::Sketch(Sketch {
6676                args: SketchCtor {
6677                    on: Plane::Default(PlaneName::Xy),
6678                },
6679                plane: ObjectId(0),
6680                segments: vec![],
6681                constraints: vec![],
6682            })
6683        );
6684        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6685
6686        let arc_ctor = ArcCtor {
6687            start: Point2d {
6688                x: Expr::Var(Number {
6689                    value: 0.0,
6690                    units: NumericSuffix::Mm,
6691                }),
6692                y: Expr::Var(Number {
6693                    value: 0.0,
6694                    units: NumericSuffix::Mm,
6695                }),
6696            },
6697            end: Point2d {
6698                x: Expr::Var(Number {
6699                    value: 10.0,
6700                    units: NumericSuffix::Mm,
6701                }),
6702                y: Expr::Var(Number {
6703                    value: 10.0,
6704                    units: NumericSuffix::Mm,
6705                }),
6706            },
6707            center: Point2d {
6708                x: Expr::Var(Number {
6709                    value: 10.0,
6710                    units: NumericSuffix::Mm,
6711                }),
6712                y: Expr::Var(Number {
6713                    value: 0.0,
6714                    units: NumericSuffix::Mm,
6715                }),
6716            },
6717            construction: None,
6718        };
6719        let segment = SegmentCtor::Arc(arc_ctor);
6720        let (src_delta, scene_delta) = frontend
6721            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6722            .await
6723            .unwrap();
6724        assert_eq!(
6725            src_delta.text.as_str(),
6726            "sketch001 = sketch(on = XY) {
6727  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6728}
6729"
6730        );
6731        assert_eq!(
6732            scene_delta.new_objects,
6733            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6734        );
6735        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6736            assert_eq!(scene_object.id.0, i);
6737        }
6738        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6739
6740        // The new objects are the end points, the center, and then the arc.
6741        let arc = *scene_delta.new_objects.last().unwrap();
6742
6743        let arc_ctor = ArcCtor {
6744            start: Point2d {
6745                x: Expr::Var(Number {
6746                    value: 1.0,
6747                    units: NumericSuffix::Mm,
6748                }),
6749                y: Expr::Var(Number {
6750                    value: 2.0,
6751                    units: NumericSuffix::Mm,
6752                }),
6753            },
6754            end: Point2d {
6755                x: Expr::Var(Number {
6756                    value: 13.0,
6757                    units: NumericSuffix::Mm,
6758                }),
6759                y: Expr::Var(Number {
6760                    value: 14.0,
6761                    units: NumericSuffix::Mm,
6762                }),
6763            },
6764            center: Point2d {
6765                x: Expr::Var(Number {
6766                    value: 13.0,
6767                    units: NumericSuffix::Mm,
6768                }),
6769                y: Expr::Var(Number {
6770                    value: 2.0,
6771                    units: NumericSuffix::Mm,
6772                }),
6773            },
6774            construction: None,
6775        };
6776        let segments = vec![ExistingSegmentCtor {
6777            id: arc,
6778            ctor: SegmentCtor::Arc(arc_ctor),
6779        }];
6780        let (src_delta, scene_delta) = frontend
6781            .edit_segments(&mock_ctx, version, sketch_id, segments)
6782            .await
6783            .unwrap();
6784        assert_eq!(
6785            src_delta.text.as_str(),
6786            "sketch001 = sketch(on = XY) {
6787  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6788}
6789"
6790        );
6791        assert_eq!(scene_delta.new_objects, vec![]);
6792        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6793
6794        ctx.close().await;
6795        mock_ctx.close().await;
6796    }
6797
6798    #[tokio::test(flavor = "multi_thread")]
6799    async fn test_new_sketch_add_circle_edit_circle() {
6800        let program = Program::empty();
6801
6802        let mut frontend = FrontendState::new();
6803        frontend.program = program;
6804
6805        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6806        let mock_ctx = ExecutorContext::new_mock(None).await;
6807        let version = Version(0);
6808
6809        let sketch_args = SketchCtor {
6810            on: Plane::Default(PlaneName::Xy),
6811        };
6812        let (_src_delta, _scene_delta, sketch_id) = frontend
6813            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6814            .await
6815            .unwrap();
6816
6817        // Add a circle segment.
6818        let circle_ctor = CircleCtor {
6819            start: Point2d {
6820                x: Expr::Var(Number {
6821                    value: 5.0,
6822                    units: NumericSuffix::Mm,
6823                }),
6824                y: Expr::Var(Number {
6825                    value: 0.0,
6826                    units: NumericSuffix::Mm,
6827                }),
6828            },
6829            center: Point2d {
6830                x: Expr::Var(Number {
6831                    value: 0.0,
6832                    units: NumericSuffix::Mm,
6833                }),
6834                y: Expr::Var(Number {
6835                    value: 0.0,
6836                    units: NumericSuffix::Mm,
6837                }),
6838            },
6839            construction: None,
6840        };
6841        let segment = SegmentCtor::Circle(circle_ctor);
6842        let (src_delta, scene_delta) = frontend
6843            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6844            .await
6845            .unwrap();
6846        assert_eq!(
6847            src_delta.text.as_str(),
6848            "sketch001 = sketch(on = XY) {
6849  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6850}
6851"
6852        );
6853        // The new objects are start, center, and then the circle segment.
6854        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6855        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6856
6857        let circle = *scene_delta.new_objects.last().unwrap();
6858
6859        // Edit the circle segment.
6860        let circle_ctor = CircleCtor {
6861            start: Point2d {
6862                x: Expr::Var(Number {
6863                    value: 10.0,
6864                    units: NumericSuffix::Mm,
6865                }),
6866                y: Expr::Var(Number {
6867                    value: 0.0,
6868                    units: NumericSuffix::Mm,
6869                }),
6870            },
6871            center: Point2d {
6872                x: Expr::Var(Number {
6873                    value: 3.0,
6874                    units: NumericSuffix::Mm,
6875                }),
6876                y: Expr::Var(Number {
6877                    value: 4.0,
6878                    units: NumericSuffix::Mm,
6879                }),
6880            },
6881            construction: None,
6882        };
6883        let segments = vec![ExistingSegmentCtor {
6884            id: circle,
6885            ctor: SegmentCtor::Circle(circle_ctor),
6886        }];
6887        let (src_delta, scene_delta) = frontend
6888            .edit_segments(&mock_ctx, version, sketch_id, segments)
6889            .await
6890            .unwrap();
6891        assert_eq!(
6892            src_delta.text.as_str(),
6893            "sketch001 = sketch(on = XY) {
6894  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6895}
6896"
6897        );
6898        assert_eq!(scene_delta.new_objects, vec![]);
6899        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6900
6901        ctx.close().await;
6902        mock_ctx.close().await;
6903    }
6904
6905    #[tokio::test(flavor = "multi_thread")]
6906    async fn test_delete_circle() {
6907        let initial_source = "sketch001 = sketch(on = XY) {
6908  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6909}
6910";
6911
6912        let program = Program::parse(initial_source).unwrap().0.unwrap();
6913        let mut frontend = FrontendState::new();
6914
6915        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6916        let mock_ctx = ExecutorContext::new_mock(None).await;
6917        let version = Version(0);
6918
6919        frontend.hack_set_program(&ctx, program).await.unwrap();
6920        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6921        let sketch_id = sketch_object.id;
6922        let sketch = expect_sketch(sketch_object);
6923
6924        // The sketch should have 3 segments: start point, center point, and the circle.
6925        assert_eq!(sketch.segments.len(), 3);
6926        let circle_id = sketch.segments[2];
6927
6928        // Delete the circle.
6929        let (src_delta, scene_delta) = frontend
6930            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6931            .await
6932            .unwrap();
6933        assert_eq!(
6934            src_delta.text.as_str(),
6935            "sketch001 = sketch(on = XY) {
6936}
6937"
6938        );
6939        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6940        let new_sketch = expect_sketch(new_sketch_object);
6941        assert_eq!(new_sketch.segments.len(), 0);
6942
6943        ctx.close().await;
6944        mock_ctx.close().await;
6945    }
6946
6947    #[tokio::test(flavor = "multi_thread")]
6948    async fn test_edit_circle_via_point() {
6949        let initial_source = "sketch001 = sketch(on = XY) {
6950  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6951}
6952";
6953
6954        let program = Program::parse(initial_source).unwrap().0.unwrap();
6955        let mut frontend = FrontendState::new();
6956
6957        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6958        let mock_ctx = ExecutorContext::new_mock(None).await;
6959        let version = Version(0);
6960
6961        frontend.hack_set_program(&ctx, program).await.unwrap();
6962        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6963        let sketch_id = sketch_object.id;
6964        let sketch = expect_sketch(sketch_object);
6965
6966        // Find the circle segment and its start point.
6967        let circle_id = sketch
6968            .segments
6969            .iter()
6970            .copied()
6971            .find(|seg_id| {
6972                matches!(
6973                    &frontend.scene_graph.objects[seg_id.0].kind,
6974                    ObjectKind::Segment {
6975                        segment: Segment::Circle(_)
6976                    }
6977                )
6978            })
6979            .expect("Expected a circle segment in sketch");
6980        let circle_object = &frontend.scene_graph.objects[circle_id.0];
6981        let ObjectKind::Segment {
6982            segment: Segment::Circle(circle),
6983        } = &circle_object.kind
6984        else {
6985            panic!("Expected circle segment, got: {:?}", circle_object.kind);
6986        };
6987        let start_point_id = circle.start;
6988
6989        // Edit the start point via SegmentCtor::Point.
6990        let segments = vec![ExistingSegmentCtor {
6991            id: start_point_id,
6992            ctor: SegmentCtor::Point(PointCtor {
6993                position: Point2d {
6994                    x: Expr::Var(Number {
6995                        value: 7.0,
6996                        units: NumericSuffix::Mm,
6997                    }),
6998                    y: Expr::Var(Number {
6999                        value: 1.0,
7000                        units: NumericSuffix::Mm,
7001                    }),
7002                },
7003            }),
7004        }];
7005        let (src_delta, _scene_delta) = frontend
7006            .edit_segments(&mock_ctx, version, sketch_id, segments)
7007            .await
7008            .unwrap();
7009        assert_eq!(
7010            src_delta.text.as_str(),
7011            "sketch001 = sketch(on = XY) {
7012  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7013}
7014"
7015        );
7016
7017        ctx.close().await;
7018        mock_ctx.close().await;
7019    }
7020
7021    #[tokio::test(flavor = "multi_thread")]
7022    async fn test_add_line_when_sketch_block_uses_variable() {
7023        let initial_source = "s = sketch(on = XY) {}
7024";
7025
7026        let program = Program::parse(initial_source).unwrap().0.unwrap();
7027
7028        let mut frontend = FrontendState::new();
7029
7030        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7031        let mock_ctx = ExecutorContext::new_mock(None).await;
7032        let version = Version(0);
7033
7034        frontend.hack_set_program(&ctx, program).await.unwrap();
7035        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7036        let sketch_id = sketch_object.id;
7037
7038        let line_ctor = LineCtor {
7039            start: Point2d {
7040                x: Expr::Number(Number {
7041                    value: 0.0,
7042                    units: NumericSuffix::Mm,
7043                }),
7044                y: Expr::Number(Number {
7045                    value: 0.0,
7046                    units: NumericSuffix::Mm,
7047                }),
7048            },
7049            end: Point2d {
7050                x: Expr::Number(Number {
7051                    value: 10.0,
7052                    units: NumericSuffix::Mm,
7053                }),
7054                y: Expr::Number(Number {
7055                    value: 10.0,
7056                    units: NumericSuffix::Mm,
7057                }),
7058            },
7059            construction: None,
7060        };
7061        let segment = SegmentCtor::Line(line_ctor);
7062        let (src_delta, scene_delta) = frontend
7063            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7064            .await
7065            .unwrap();
7066        assert_eq!(
7067            src_delta.text.as_str(),
7068            "s = sketch(on = XY) {
7069  line(start = [0mm, 0mm], end = [10mm, 10mm])
7070}
7071"
7072        );
7073        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7074        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7075
7076        ctx.close().await;
7077        mock_ctx.close().await;
7078    }
7079
7080    #[tokio::test(flavor = "multi_thread")]
7081    async fn test_new_sketch_add_line_delete_sketch() {
7082        let program = Program::empty();
7083
7084        let mut frontend = FrontendState::new();
7085        frontend.program = program;
7086
7087        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7088        let mock_ctx = ExecutorContext::new_mock(None).await;
7089        let version = Version(0);
7090
7091        let sketch_args = SketchCtor {
7092            on: Plane::Default(PlaneName::Xy),
7093        };
7094        let (_src_delta, scene_delta, sketch_id) = frontend
7095            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7096            .await
7097            .unwrap();
7098        assert_eq!(sketch_id, ObjectId(1));
7099        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7100        let sketch_object = &scene_delta.new_graph.objects[1];
7101        assert_eq!(sketch_object.id, ObjectId(1));
7102        assert_eq!(
7103            sketch_object.kind,
7104            ObjectKind::Sketch(Sketch {
7105                args: SketchCtor {
7106                    on: Plane::Default(PlaneName::Xy)
7107                },
7108                plane: ObjectId(0),
7109                segments: vec![],
7110                constraints: vec![],
7111            })
7112        );
7113        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7114
7115        let line_ctor = LineCtor {
7116            start: Point2d {
7117                x: Expr::Number(Number {
7118                    value: 0.0,
7119                    units: NumericSuffix::Mm,
7120                }),
7121                y: Expr::Number(Number {
7122                    value: 0.0,
7123                    units: NumericSuffix::Mm,
7124                }),
7125            },
7126            end: Point2d {
7127                x: Expr::Number(Number {
7128                    value: 10.0,
7129                    units: NumericSuffix::Mm,
7130                }),
7131                y: Expr::Number(Number {
7132                    value: 10.0,
7133                    units: NumericSuffix::Mm,
7134                }),
7135            },
7136            construction: None,
7137        };
7138        let segment = SegmentCtor::Line(line_ctor);
7139        let (src_delta, scene_delta) = frontend
7140            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7141            .await
7142            .unwrap();
7143        assert_eq!(
7144            src_delta.text.as_str(),
7145            "sketch001 = sketch(on = XY) {
7146  line(start = [0mm, 0mm], end = [10mm, 10mm])
7147}
7148"
7149        );
7150        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7151
7152        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7153        assert_eq!(src_delta.text.as_str(), "");
7154        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7155
7156        ctx.close().await;
7157        mock_ctx.close().await;
7158    }
7159
7160    #[tokio::test(flavor = "multi_thread")]
7161    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7162        let initial_source = "s = sketch(on = XY) {}
7163";
7164
7165        let program = Program::parse(initial_source).unwrap().0.unwrap();
7166
7167        let mut frontend = FrontendState::new();
7168
7169        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7170        let mock_ctx = ExecutorContext::new_mock(None).await;
7171        let version = Version(0);
7172
7173        frontend.hack_set_program(&ctx, program).await.unwrap();
7174        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7175        let sketch_id = sketch_object.id;
7176
7177        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7178        assert_eq!(src_delta.text.as_str(), "");
7179        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7180
7181        ctx.close().await;
7182        mock_ctx.close().await;
7183    }
7184
7185    #[tokio::test(flavor = "multi_thread")]
7186    async fn test_delete_sketch_after_comment() {
7187        let initial_source = "sketch001 = sketch(on = XZ) {
7188}
7189";
7190
7191        let program = Program::parse(initial_source).unwrap().0.unwrap();
7192        let mut frontend = FrontendState::new();
7193
7194        let ctx = ExecutorContext::new_with_engine(
7195            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7196            Default::default(),
7197        );
7198        let version = Version(0);
7199
7200        frontend.hack_set_program(&ctx, program).await.unwrap();
7201        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7202        let sketch_id = sketch_object.id;
7203        let original_source = sketch_object.source.clone();
7204
7205        let commented_source = "// test 1
7206sketch001 = sketch(on = XZ) {
7207}
7208";
7209        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7210        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7211
7212        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7213        assert_eq!(cached_sketch_object.source, original_source);
7214
7215        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7216        assert!(
7217            !src_delta.text.contains("sketch001"),
7218            "sketch was not deleted: {}",
7219            src_delta.text
7220        );
7221        // The leading line comment must survive deletion.
7222        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7223        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7224
7225        ctx.close().await;
7226    }
7227
7228    #[tokio::test(flavor = "multi_thread")]
7229    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7230        let initial_source = "sketch001 = sketch(on = XZ) {
7231}
7232foo = 1
7233";
7234
7235        let program = Program::parse(initial_source).unwrap().0.unwrap();
7236        let mut frontend = FrontendState::new();
7237
7238        let ctx = ExecutorContext::new_with_engine(
7239            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7240            Default::default(),
7241        );
7242        let version = Version(0);
7243
7244        frontend.hack_set_program(&ctx, program).await.unwrap();
7245        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7246        let sketch_id = sketch_object.id;
7247
7248        let commented_source = "// keep me
7249sketch001 = sketch(on = XZ) {
7250}
7251foo = 1
7252";
7253        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7254        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7255
7256        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7257        // The leading comment should remain, now attached to the following body item.
7258        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7259
7260        ctx.close().await;
7261    }
7262
7263    #[tokio::test(flavor = "multi_thread")]
7264    async fn test_delete_segment_preserves_pre_comment() {
7265        let initial_source = "\
7266sketch(on = XY) {
7267  point(at = [var 1, var 2])
7268  // describe the middle point
7269  point(at = [var 3, var 4])
7270  point(at = [var 5, var 6])
7271}
7272";
7273
7274        let program = Program::parse(initial_source).unwrap().0.unwrap();
7275        let mut frontend = FrontendState::new();
7276
7277        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7278        let mock_ctx = ExecutorContext::new_mock(None).await;
7279        let version = Version(0);
7280
7281        frontend.hack_set_program(&ctx, program).await.unwrap();
7282        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7283        let sketch_id = sketch_object.id;
7284        let sketch = expect_sketch(sketch_object);
7285
7286        let middle_point_id = *sketch.segments.get(1).unwrap();
7287
7288        let (src_delta, _scene_delta) = frontend
7289            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7290            .await
7291            .unwrap();
7292        // The line comment on the line above the deleted point must be preserved.
7293        // It is reattached to the next surviving body item.
7294        assert_eq!(
7295            src_delta.text.as_str(),
7296            "\
7297sketch(on = XY) {
7298  point(at = [var 1mm, var 2mm])
7299  // describe the middle point
7300  point(at = [var 5mm, var 6mm])
7301}
7302"
7303        );
7304
7305        ctx.close().await;
7306        mock_ctx.close().await;
7307    }
7308
7309    #[tokio::test(flavor = "multi_thread")]
7310    async fn test_delete_last_segment_preserves_pre_comment() {
7311        let initial_source = "\
7312sketch(on = XY) {
7313  point(at = [var 1, var 2])
7314  // describe the trailing point
7315  point(at = [var 3, var 4])
7316}
7317";
7318
7319        let program = Program::parse(initial_source).unwrap().0.unwrap();
7320        let mut frontend = FrontendState::new();
7321
7322        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7323        let mock_ctx = ExecutorContext::new_mock(None).await;
7324        let version = Version(0);
7325
7326        frontend.hack_set_program(&ctx, program).await.unwrap();
7327        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7328        let sketch_id = sketch_object.id;
7329        let sketch = expect_sketch(sketch_object);
7330
7331        let last_point_id = *sketch.segments.last().unwrap();
7332
7333        let (src_delta, _scene_delta) = frontend
7334            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7335            .await
7336            .unwrap();
7337        // No following item to attach to; the comment is kept inside the sketch
7338        // block as trailing non-code metadata so the user does not lose it.
7339        assert_eq!(
7340            src_delta.text.as_str(),
7341            "\
7342sketch(on = XY) {
7343  point(at = [var 1mm, var 2mm])
7344  // describe the trailing point
7345}
7346"
7347        );
7348
7349        ctx.close().await;
7350        mock_ctx.close().await;
7351    }
7352
7353    #[tokio::test(flavor = "multi_thread")]
7354    async fn test_delete_segment_drops_inline_trailing_comment() {
7355        let initial_source = "\
7356sketch(on = XY) {
7357  point(at = [var 1, var 2])
7358  point(at = [var 3, var 4]) // same-line note that gets dropped
7359  point(at = [var 5, var 6])
7360}
7361";
7362
7363        let program = Program::parse(initial_source).unwrap().0.unwrap();
7364        let mut frontend = FrontendState::new();
7365
7366        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7367        let mock_ctx = ExecutorContext::new_mock(None).await;
7368        let version = Version(0);
7369
7370        frontend.hack_set_program(&ctx, program).await.unwrap();
7371        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7372        let sketch_id = sketch_object.id;
7373        let sketch = expect_sketch(sketch_object);
7374
7375        let middle_point_id = *sketch.segments.get(1).unwrap();
7376
7377        let (src_delta, _scene_delta) = frontend
7378            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7379            .await
7380            .unwrap();
7381        // The same-line trailing comment is removed along with the deleted code.
7382        assert!(
7383            !src_delta.text.contains("same-line note"),
7384            "inline comment should have been removed: {}",
7385            src_delta.text
7386        );
7387
7388        ctx.close().await;
7389        mock_ctx.close().await;
7390    }
7391
7392    #[tokio::test(flavor = "multi_thread")]
7393    async fn test_delete_segments_preserves_block_comments_across_positions() {
7394        // One test exercising several `delete_body_item_preserving_pre_comments`
7395        // branches at once with `/* ... */` block comments:
7396        //   - first point: leading block comment must migrate to the next item.
7397        //   - first point: same-line trailing block comment must be dropped.
7398        //   - middle point: leading block comment must stay attached after migration.
7399        //   - last point: leading block comment, with no surviving next item,
7400        //     must be converted into a trailing NonCodeNode.
7401        let initial_source = "\
7402sketch(on = XY) {
7403  /* above first - moves to middle */
7404  point(at = [var 1, var 2]) /* same-line on first - dropped */
7405  /* above middle - stays */
7406  point(at = [var 3, var 4])
7407  /* above last - moves to trailing meta */
7408  point(at = [var 5, var 6])
7409}
7410";
7411
7412        let program = Program::parse(initial_source).unwrap().0.unwrap();
7413        let mut frontend = FrontendState::new();
7414
7415        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7416        let mock_ctx = ExecutorContext::new_mock(None).await;
7417        let version = Version(0);
7418
7419        frontend.hack_set_program(&ctx, program).await.unwrap();
7420        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7421        let sketch_id = sketch_object.id;
7422        let sketch = expect_sketch(sketch_object);
7423
7424        let first_point_id = *sketch.segments.first().unwrap();
7425        let last_point_id = *sketch.segments.last().unwrap();
7426
7427        let (src_delta, _scene_delta) = frontend
7428            .delete_objects(
7429                &mock_ctx,
7430                version,
7431                sketch_id,
7432                Vec::new(),
7433                vec![first_point_id, last_point_id],
7434            )
7435            .await
7436            .unwrap();
7437        assert_eq!(
7438            src_delta.text.as_str(),
7439            "\
7440sketch(on = XY) {
7441  /* above first - moves to middle */
7442  /* above middle - stays */
7443  point(at = [var 3mm, var 4mm])
7444  /* above last - moves to trailing meta */
7445}
7446"
7447        );
7448
7449        ctx.close().await;
7450        mock_ctx.close().await;
7451    }
7452
7453    #[tokio::test(flavor = "multi_thread")]
7454    async fn test_edit_line_when_editing_its_start_point() {
7455        let initial_source = "\
7456sketch(on = XY) {
7457  line(start = [var 1, var 2], end = [var 3, var 4])
7458}
7459";
7460
7461        let program = Program::parse(initial_source).unwrap().0.unwrap();
7462
7463        let mut frontend = FrontendState::new();
7464
7465        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7466        let mock_ctx = ExecutorContext::new_mock(None).await;
7467        let version = Version(0);
7468
7469        frontend.hack_set_program(&ctx, program).await.unwrap();
7470        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7471        let sketch_id = sketch_object.id;
7472        let sketch = expect_sketch(sketch_object);
7473
7474        let point_id = *sketch.segments.first().unwrap();
7475
7476        let point_ctor = PointCtor {
7477            position: Point2d {
7478                x: Expr::Var(Number {
7479                    value: 5.0,
7480                    units: NumericSuffix::Inch,
7481                }),
7482                y: Expr::Var(Number {
7483                    value: 6.0,
7484                    units: NumericSuffix::Inch,
7485                }),
7486            },
7487        };
7488        let segments = vec![ExistingSegmentCtor {
7489            id: point_id,
7490            ctor: SegmentCtor::Point(point_ctor),
7491        }];
7492        let (src_delta, scene_delta) = frontend
7493            .edit_segments(&mock_ctx, version, sketch_id, segments)
7494            .await
7495            .unwrap();
7496        assert_eq!(
7497            src_delta.text.as_str(),
7498            "\
7499sketch(on = XY) {
7500  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7501}
7502"
7503        );
7504        assert_eq!(scene_delta.new_objects, vec![]);
7505        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7506
7507        ctx.close().await;
7508        mock_ctx.close().await;
7509    }
7510
7511    #[tokio::test(flavor = "multi_thread")]
7512    async fn test_edit_line_when_editing_its_end_point() {
7513        let initial_source = "\
7514sketch(on = XY) {
7515  line(start = [var 1, var 2], end = [var 3, var 4])
7516}
7517";
7518
7519        let program = Program::parse(initial_source).unwrap().0.unwrap();
7520
7521        let mut frontend = FrontendState::new();
7522
7523        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7524        let mock_ctx = ExecutorContext::new_mock(None).await;
7525        let version = Version(0);
7526
7527        frontend.hack_set_program(&ctx, program).await.unwrap();
7528        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7529        let sketch_id = sketch_object.id;
7530        let sketch = expect_sketch(sketch_object);
7531        let point_id = *sketch.segments.get(1).unwrap();
7532
7533        let point_ctor = PointCtor {
7534            position: Point2d {
7535                x: Expr::Var(Number {
7536                    value: 5.0,
7537                    units: NumericSuffix::Inch,
7538                }),
7539                y: Expr::Var(Number {
7540                    value: 6.0,
7541                    units: NumericSuffix::Inch,
7542                }),
7543            },
7544        };
7545        let segments = vec![ExistingSegmentCtor {
7546            id: point_id,
7547            ctor: SegmentCtor::Point(point_ctor),
7548        }];
7549        let (src_delta, scene_delta) = frontend
7550            .edit_segments(&mock_ctx, version, sketch_id, segments)
7551            .await
7552            .unwrap();
7553        assert_eq!(
7554            src_delta.text.as_str(),
7555            "\
7556sketch(on = XY) {
7557  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7558}
7559"
7560        );
7561        assert_eq!(scene_delta.new_objects, vec![]);
7562        assert_eq!(
7563            scene_delta.new_graph.objects.len(),
7564            5,
7565            "{:#?}",
7566            scene_delta.new_graph.objects
7567        );
7568
7569        ctx.close().await;
7570        mock_ctx.close().await;
7571    }
7572
7573    #[tokio::test(flavor = "multi_thread")]
7574    async fn test_edit_line_with_coincident_feedback() {
7575        let initial_source = "\
7576sketch(on = XY) {
7577  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7578  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7579  fixed([line1.start, [0, 0]])
7580  coincident([line1.end, line2.start])
7581  equalLength([line1, line2])
7582}
7583";
7584
7585        let program = Program::parse(initial_source).unwrap().0.unwrap();
7586
7587        let mut frontend = FrontendState::new();
7588
7589        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7590        let mock_ctx = ExecutorContext::new_mock(None).await;
7591        let version = Version(0);
7592
7593        frontend.hack_set_program(&ctx, program).await.unwrap();
7594        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7595        let sketch_id = sketch_object.id;
7596        let sketch = expect_sketch(sketch_object);
7597        let line2_end_id = *sketch.segments.get(4).unwrap();
7598
7599        let segments = vec![ExistingSegmentCtor {
7600            id: line2_end_id,
7601            ctor: SegmentCtor::Point(PointCtor {
7602                position: Point2d {
7603                    x: Expr::Var(Number {
7604                        value: 9.0,
7605                        units: NumericSuffix::None,
7606                    }),
7607                    y: Expr::Var(Number {
7608                        value: 10.0,
7609                        units: NumericSuffix::None,
7610                    }),
7611                },
7612            }),
7613        }];
7614        let (src_delta, scene_delta) = frontend
7615            .edit_segments(&mock_ctx, version, sketch_id, segments)
7616            .await
7617            .unwrap();
7618        assert_eq!(
7619            src_delta.text.as_str(),
7620            "\
7621sketch(on = XY) {
7622  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7623  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7624  fixed([line1.start, [0, 0]])
7625  coincident([line1.end, line2.start])
7626  equalLength([line1, line2])
7627}
7628"
7629        );
7630        assert_eq!(
7631            scene_delta.new_graph.objects.len(),
7632            11,
7633            "{:#?}",
7634            scene_delta.new_graph.objects
7635        );
7636
7637        ctx.close().await;
7638        mock_ctx.close().await;
7639    }
7640
7641    #[tokio::test(flavor = "multi_thread")]
7642    async fn test_delete_point_without_var() {
7643        let initial_source = "\
7644sketch(on = XY) {
7645  point(at = [var 1, var 2])
7646  point(at = [var 3, var 4])
7647  point(at = [var 5, var 6])
7648}
7649";
7650
7651        let program = Program::parse(initial_source).unwrap().0.unwrap();
7652
7653        let mut frontend = FrontendState::new();
7654
7655        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7656        let mock_ctx = ExecutorContext::new_mock(None).await;
7657        let version = Version(0);
7658
7659        frontend.hack_set_program(&ctx, program).await.unwrap();
7660        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7661        let sketch_id = sketch_object.id;
7662        let sketch = expect_sketch(sketch_object);
7663
7664        let point_id = *sketch.segments.get(1).unwrap();
7665
7666        let (src_delta, scene_delta) = frontend
7667            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7668            .await
7669            .unwrap();
7670        assert_eq!(
7671            src_delta.text.as_str(),
7672            "\
7673sketch(on = XY) {
7674  point(at = [var 1mm, var 2mm])
7675  point(at = [var 5mm, var 6mm])
7676}
7677"
7678        );
7679        assert_eq!(scene_delta.new_objects, vec![]);
7680        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7681
7682        ctx.close().await;
7683        mock_ctx.close().await;
7684    }
7685
7686    #[tokio::test(flavor = "multi_thread")]
7687    async fn test_delete_point_with_var() {
7688        let initial_source = "\
7689sketch(on = XY) {
7690  point(at = [var 1, var 2])
7691  point1 = point(at = [var 3, var 4])
7692  point(at = [var 5, var 6])
7693}
7694";
7695
7696        let program = Program::parse(initial_source).unwrap().0.unwrap();
7697
7698        let mut frontend = FrontendState::new();
7699
7700        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7701        let mock_ctx = ExecutorContext::new_mock(None).await;
7702        let version = Version(0);
7703
7704        frontend.hack_set_program(&ctx, program).await.unwrap();
7705        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7706        let sketch_id = sketch_object.id;
7707        let sketch = expect_sketch(sketch_object);
7708
7709        let point_id = *sketch.segments.get(1).unwrap();
7710
7711        let (src_delta, scene_delta) = frontend
7712            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7713            .await
7714            .unwrap();
7715        assert_eq!(
7716            src_delta.text.as_str(),
7717            "\
7718sketch(on = XY) {
7719  point(at = [var 1mm, var 2mm])
7720  point(at = [var 5mm, var 6mm])
7721}
7722"
7723        );
7724        assert_eq!(scene_delta.new_objects, vec![]);
7725        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7726
7727        ctx.close().await;
7728        mock_ctx.close().await;
7729    }
7730
7731    #[tokio::test(flavor = "multi_thread")]
7732    async fn test_delete_multiple_points() {
7733        let initial_source = "\
7734sketch(on = XY) {
7735  point(at = [var 1, var 2])
7736  point1 = point(at = [var 3, var 4])
7737  point(at = [var 5, var 6])
7738}
7739";
7740
7741        let program = Program::parse(initial_source).unwrap().0.unwrap();
7742
7743        let mut frontend = FrontendState::new();
7744
7745        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7746        let mock_ctx = ExecutorContext::new_mock(None).await;
7747        let version = Version(0);
7748
7749        frontend.hack_set_program(&ctx, program).await.unwrap();
7750        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7751        let sketch_id = sketch_object.id;
7752
7753        let sketch = expect_sketch(sketch_object);
7754
7755        let point1_id = *sketch.segments.first().unwrap();
7756        let point2_id = *sketch.segments.get(1).unwrap();
7757
7758        let (src_delta, scene_delta) = frontend
7759            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7760            .await
7761            .unwrap();
7762        assert_eq!(
7763            src_delta.text.as_str(),
7764            "\
7765sketch(on = XY) {
7766  point(at = [var 5mm, var 6mm])
7767}
7768"
7769        );
7770        assert_eq!(scene_delta.new_objects, vec![]);
7771        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7772
7773        ctx.close().await;
7774        mock_ctx.close().await;
7775    }
7776
7777    #[tokio::test(flavor = "multi_thread")]
7778    async fn test_delete_coincident_constraint() {
7779        let initial_source = "\
7780sketch(on = XY) {
7781  point1 = point(at = [var 1, var 2])
7782  point2 = point(at = [var 3, var 4])
7783  coincident([point1, point2])
7784  point(at = [var 5, var 6])
7785}
7786";
7787
7788        let program = Program::parse(initial_source).unwrap().0.unwrap();
7789
7790        let mut frontend = FrontendState::new();
7791
7792        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793        let mock_ctx = ExecutorContext::new_mock(None).await;
7794        let version = Version(0);
7795
7796        frontend.hack_set_program(&ctx, program).await.unwrap();
7797        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7798        let sketch_id = sketch_object.id;
7799        let sketch = expect_sketch(sketch_object);
7800
7801        let coincident_id = *sketch.constraints.first().unwrap();
7802
7803        let (src_delta, scene_delta) = frontend
7804            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7805            .await
7806            .unwrap();
7807        assert_eq!(
7808            src_delta.text.as_str(),
7809            "\
7810sketch(on = XY) {
7811  point1 = point(at = [var 1mm, var 2mm])
7812  point2 = point(at = [var 3mm, var 4mm])
7813  point(at = [var 5mm, var 6mm])
7814}
7815"
7816        );
7817        assert_eq!(scene_delta.new_objects, vec![]);
7818        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7819
7820        ctx.close().await;
7821        mock_ctx.close().await;
7822    }
7823
7824    #[tokio::test(flavor = "multi_thread")]
7825    async fn test_delete_line_cascades_to_coincident_constraint() {
7826        let initial_source = "\
7827sketch(on = XY) {
7828  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7829  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7830  coincident([line1.end, line2.start])
7831}
7832";
7833
7834        let program = Program::parse(initial_source).unwrap().0.unwrap();
7835
7836        let mut frontend = FrontendState::new();
7837
7838        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7839        let mock_ctx = ExecutorContext::new_mock(None).await;
7840        let version = Version(0);
7841
7842        frontend.hack_set_program(&ctx, program).await.unwrap();
7843        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7844        let sketch_id = sketch_object.id;
7845        let sketch = expect_sketch(sketch_object);
7846        let line_id = *sketch.segments.get(5).unwrap();
7847
7848        let (src_delta, scene_delta) = frontend
7849            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7850            .await
7851            .unwrap();
7852        assert_eq!(
7853            src_delta.text.as_str(),
7854            "\
7855sketch(on = XY) {
7856  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7857}
7858"
7859        );
7860        assert_eq!(
7861            scene_delta.new_graph.objects.len(),
7862            5,
7863            "{:#?}",
7864            scene_delta.new_graph.objects
7865        );
7866
7867        ctx.close().await;
7868        mock_ctx.close().await;
7869    }
7870
7871    #[tokio::test(flavor = "multi_thread")]
7872    async fn test_delete_line_cascades_to_distance_constraint() {
7873        let initial_source = "\
7874sketch(on = XY) {
7875  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7876  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7877  distance([line1.end, line2.start]) == 10mm
7878}
7879";
7880
7881        let program = Program::parse(initial_source).unwrap().0.unwrap();
7882
7883        let mut frontend = FrontendState::new();
7884
7885        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7886        let mock_ctx = ExecutorContext::new_mock(None).await;
7887        let version = Version(0);
7888
7889        frontend.hack_set_program(&ctx, program).await.unwrap();
7890        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7891        let sketch_id = sketch_object.id;
7892        let sketch = expect_sketch(sketch_object);
7893        let line_id = *sketch.segments.get(5).unwrap();
7894
7895        let (src_delta, scene_delta) = frontend
7896            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7897            .await
7898            .unwrap();
7899        assert_eq!(
7900            src_delta.text.as_str(),
7901            "\
7902sketch(on = XY) {
7903  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7904}
7905"
7906        );
7907        assert_eq!(
7908            scene_delta.new_graph.objects.len(),
7909            5,
7910            "{:#?}",
7911            scene_delta.new_graph.objects
7912        );
7913
7914        ctx.close().await;
7915        mock_ctx.close().await;
7916    }
7917
7918    #[tokio::test(flavor = "multi_thread")]
7919    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7920        let initial_source = "\
7921sketch(on = XY) {
7922  point1 = point(at = [var 1, var 2])
7923  point2 = point(at = [var 3, var 4])
7924  horizontalDistance([point1, point2]) == 10mm
7925}
7926";
7927
7928        let program = Program::parse(initial_source).unwrap().0.unwrap();
7929
7930        let mut frontend = FrontendState::new();
7931
7932        let mock_ctx = ExecutorContext::new_mock(None).await;
7933        let version = Version(0);
7934
7935        frontend.program = program.clone();
7936        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7937        frontend.update_state_after_exec(outcome, true);
7938        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7939        let sketch_id = sketch_object.id;
7940        let sketch = expect_sketch(sketch_object);
7941        let point2_id = *sketch.segments.get(1).unwrap();
7942
7943        let (src_delta, scene_delta) = frontend
7944            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7945            .await
7946            .unwrap();
7947        assert_eq!(
7948            src_delta.text.as_str(),
7949            "\
7950sketch(on = XY) {
7951  point1 = point(at = [var 1mm, var 2mm])
7952}
7953"
7954        );
7955        assert_eq!(
7956            scene_delta.new_graph.objects.len(),
7957            3,
7958            "{:#?}",
7959            scene_delta.new_graph.objects
7960        );
7961
7962        mock_ctx.close().await;
7963    }
7964
7965    #[tokio::test(flavor = "multi_thread")]
7966    async fn test_delete_line_cascades_to_fixed_constraint() {
7967        let initial_source = "\
7968sketch(on = XY) {
7969  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7970  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7971  fixed([line1.start, [0, 0]])
7972}
7973";
7974
7975        let program = Program::parse(initial_source).unwrap().0.unwrap();
7976
7977        let mut frontend = FrontendState::new();
7978
7979        let mock_ctx = ExecutorContext::new_mock(None).await;
7980        let version = Version(0);
7981
7982        frontend.program = program.clone();
7983        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7984        frontend.update_state_after_exec(outcome, true);
7985        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7986        let sketch_id = sketch_object.id;
7987        let sketch = expect_sketch(sketch_object);
7988        let line1_id = *sketch.segments.get(2).unwrap();
7989
7990        let (src_delta, scene_delta) = frontend
7991            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7992            .await
7993            .unwrap();
7994        assert_eq!(
7995            src_delta.text.as_str(),
7996            "\
7997sketch(on = XY) {
7998  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
7999}
8000"
8001        );
8002        assert_eq!(
8003            scene_delta.new_graph.objects.len(),
8004            5,
8005            "{:#?}",
8006            scene_delta.new_graph.objects
8007        );
8008
8009        mock_ctx.close().await;
8010    }
8011
8012    #[tokio::test(flavor = "multi_thread")]
8013    async fn test_delete_line_cascades_to_midpoint_constraint() {
8014        let initial_source = "\
8015sketch(on = XY) {
8016  point1 = point(at = [var 1, var 2])
8017  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8018  midpoint(line1, point = point1)
8019}
8020";
8021
8022        let program = Program::parse(initial_source).unwrap().0.unwrap();
8023
8024        let mut frontend = FrontendState::new();
8025
8026        let mock_ctx = ExecutorContext::new_mock(None).await;
8027        let version = Version(0);
8028
8029        frontend.program = program.clone();
8030        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8031        frontend.update_state_after_exec(outcome, true);
8032        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8033        let sketch_id = sketch_object.id;
8034        let sketch = expect_sketch(sketch_object);
8035        let line1_id = *sketch.segments.get(3).unwrap();
8036
8037        let (src_delta, scene_delta) = frontend
8038            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8039            .await
8040            .unwrap();
8041        assert_eq!(
8042            src_delta.text.as_str(),
8043            "\
8044sketch(on = XY) {
8045  point1 = point(at = [var 1mm, var 2mm])
8046}
8047"
8048        );
8049        assert_eq!(
8050            scene_delta.new_graph.objects.len(),
8051            3,
8052            "{:#?}",
8053            scene_delta.new_graph.objects
8054        );
8055
8056        mock_ctx.close().await;
8057    }
8058
8059    #[tokio::test(flavor = "multi_thread")]
8060    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8061        let initial_source = "\
8062sketch(on = XY) {
8063  point1 = point(at = [var 1, var 2])
8064  point2 = point(at = [var 3, var 4])
8065  point3 = point(at = [var 5, var 6])
8066  coincident([point1, point2, point3])
8067}
8068";
8069
8070        let program = Program::parse(initial_source).unwrap().0.unwrap();
8071
8072        let mut frontend = FrontendState::new();
8073
8074        let mock_ctx = ExecutorContext::new_mock(None).await;
8075        let version = Version(0);
8076
8077        frontend.program = program.clone();
8078        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8079        frontend.update_state_after_exec(outcome, true);
8080        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8081        let sketch_id = sketch_object.id;
8082        let sketch = expect_sketch(sketch_object);
8083        let point3_id = *sketch.segments.get(2).unwrap();
8084
8085        let (src_delta, scene_delta) = frontend
8086            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8087            .await
8088            .unwrap();
8089        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8090        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8091        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8092        assert!(
8093            src_delta.text.contains("coincident([point1, point2])"),
8094            "{}",
8095            src_delta.text
8096        );
8097
8098        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8099        let sketch = expect_sketch(sketch_object);
8100        assert_eq!(sketch.segments.len(), 2);
8101        assert_eq!(sketch.constraints.len(), 1);
8102
8103        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8104        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8105            panic!("Expected constraint object");
8106        };
8107        let Constraint::Coincident(coincident) = constraint else {
8108            panic!("Expected coincident constraint");
8109        };
8110        assert_eq!(
8111            coincident.segments,
8112            sketch
8113                .segments
8114                .iter()
8115                .copied()
8116                .map(Into::into)
8117                .collect::<Vec<ConstraintSegment>>()
8118        );
8119
8120        mock_ctx.close().await;
8121    }
8122
8123    #[tokio::test(flavor = "multi_thread")]
8124    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8125        let initial_source = "\
8126sketch(on = XY) {
8127  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8128  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8129  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8130  equalLength([line1, line2, line3])
8131}
8132";
8133
8134        let program = Program::parse(initial_source).unwrap().0.unwrap();
8135
8136        let mut frontend = FrontendState::new();
8137
8138        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8139        let mock_ctx = ExecutorContext::new_mock(None).await;
8140        let version = Version(0);
8141
8142        frontend.hack_set_program(&ctx, program).await.unwrap();
8143        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8144        let sketch_id = sketch_object.id;
8145        let sketch = expect_sketch(sketch_object);
8146        let line3_id = *sketch.segments.get(8).unwrap();
8147
8148        let (src_delta, scene_delta) = frontend
8149            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8150            .await
8151            .unwrap();
8152        assert_eq!(
8153            src_delta.text.as_str(),
8154            "\
8155sketch(on = XY) {
8156  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8157  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8158  equalLength([line1, line2])
8159}
8160"
8161        );
8162
8163        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8164        let sketch = expect_sketch(sketch_object);
8165        assert_eq!(sketch.constraints.len(), 1);
8166
8167        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8168        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8169            panic!("Expected constraint object");
8170        };
8171        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8172            panic!("Expected lines equal length constraint");
8173        };
8174        assert_eq!(lines_equal_length.lines.len(), 2);
8175
8176        ctx.close().await;
8177        mock_ctx.close().await;
8178    }
8179
8180    #[tokio::test(flavor = "multi_thread")]
8181    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8182        let initial_source = "\
8183sketch(on = XY) {
8184  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8185  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8186  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8187  horizontal([line1.end, line2.start, line3.start])
8188}
8189";
8190
8191        let program = Program::parse(initial_source).unwrap().0.unwrap();
8192
8193        let mut frontend = FrontendState::new();
8194
8195        let mock_ctx = ExecutorContext::new_mock(None).await;
8196        let version = Version(0);
8197
8198        frontend.program = program.clone();
8199        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8200        frontend.update_state_after_exec(outcome, true);
8201        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8202        let sketch_id = sketch_object.id;
8203        let sketch = expect_sketch(sketch_object);
8204        let line1_id = *sketch.segments.get(2).unwrap();
8205
8206        let (src_delta, scene_delta) = frontend
8207            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8208            .await
8209            .unwrap();
8210        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8211        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8212        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8213        assert!(
8214            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8215            "{}",
8216            src_delta.text
8217        );
8218
8219        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8220        let sketch = expect_sketch(sketch_object);
8221        assert_eq!(sketch.constraints.len(), 1);
8222
8223        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8224        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8225            panic!("Expected constraint object");
8226        };
8227        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8228            panic!("Expected horizontal points constraint");
8229        };
8230        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8231        assert_eq!(*points, remaining_points);
8232
8233        mock_ctx.close().await;
8234    }
8235
8236    #[tokio::test(flavor = "multi_thread")]
8237    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8238        let initial_source = "\
8239sketch(on = XY) {
8240  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8241  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8242  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8243  vertical([line1.end, line2.start, line3.start])
8244}
8245";
8246
8247        let program = Program::parse(initial_source).unwrap().0.unwrap();
8248
8249        let mut frontend = FrontendState::new();
8250
8251        let mock_ctx = ExecutorContext::new_mock(None).await;
8252        let version = Version(0);
8253
8254        frontend.program = program.clone();
8255        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8256        frontend.update_state_after_exec(outcome, true);
8257        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8258        let sketch_id = sketch_object.id;
8259        let sketch = expect_sketch(sketch_object);
8260        let line1_id = *sketch.segments.get(2).unwrap();
8261
8262        let (src_delta, scene_delta) = frontend
8263            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8264            .await
8265            .unwrap();
8266        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8267        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8268        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8269        assert!(
8270            src_delta.text.contains("vertical([line2.start, line3.start])"),
8271            "{}",
8272            src_delta.text
8273        );
8274
8275        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8276        let sketch = expect_sketch(sketch_object);
8277        assert_eq!(sketch.constraints.len(), 1);
8278
8279        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8280        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8281            panic!("Expected constraint object");
8282        };
8283        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8284            panic!("Expected vertical points constraint");
8285        };
8286        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8287        assert_eq!(*points, remaining_points);
8288
8289        mock_ctx.close().await;
8290    }
8291
8292    #[tokio::test(flavor = "multi_thread")]
8293    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8294        let initial_source = "\
8295sketch(on = XY) {
8296  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8297  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8298  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8299  coincident([line1.end, line2.start, line3.start])
8300}
8301";
8302
8303        let program = Program::parse(initial_source).unwrap().0.unwrap();
8304
8305        let mut frontend = FrontendState::new();
8306
8307        let mock_ctx = ExecutorContext::new_mock(None).await;
8308        let version = Version(0);
8309
8310        frontend.program = program.clone();
8311        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8312        frontend.update_state_after_exec(outcome, true);
8313        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8314        let sketch_id = sketch_object.id;
8315        let sketch = expect_sketch(sketch_object);
8316        let line1_id = *sketch.segments.get(2).unwrap();
8317
8318        let (src_delta, scene_delta) = frontend
8319            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8320            .await
8321            .unwrap();
8322        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8323        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8324        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8325        assert!(
8326            src_delta.text.contains("coincident([line2.start, line3.start])"),
8327            "{}",
8328            src_delta.text
8329        );
8330
8331        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8332        let sketch = expect_sketch(sketch_object);
8333        assert_eq!(sketch.constraints.len(), 1);
8334
8335        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8336        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8337            panic!("Expected constraint object");
8338        };
8339        let Constraint::Coincident(coincident) = constraint else {
8340            panic!("Expected coincident constraint");
8341        };
8342        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8343        assert_eq!(coincident.segments, remaining_segments);
8344
8345        mock_ctx.close().await;
8346    }
8347
8348    #[tokio::test(flavor = "multi_thread")]
8349    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8350        let initial_source = "\
8351sketch(on = XY) {
8352  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8353  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8354  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8355  equalLength([line1, line2, line3])
8356}
8357";
8358
8359        let program = Program::parse(initial_source).unwrap().0.unwrap();
8360
8361        let mut frontend = FrontendState::new();
8362
8363        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8364        let mock_ctx = ExecutorContext::new_mock(None).await;
8365        let version = Version(0);
8366
8367        frontend.hack_set_program(&ctx, program).await.unwrap();
8368        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8369        let sketch_id = sketch_object.id;
8370        let sketch = expect_sketch(sketch_object);
8371        let line2_id = *sketch.segments.get(5).unwrap();
8372        let line3_id = *sketch.segments.get(8).unwrap();
8373
8374        let (src_delta, scene_delta) = frontend
8375            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8376            .await
8377            .unwrap();
8378        assert_eq!(
8379            src_delta.text.as_str(),
8380            "\
8381sketch(on = XY) {
8382  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8383}
8384"
8385        );
8386
8387        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8388        let sketch = expect_sketch(sketch_object);
8389        assert!(sketch.constraints.is_empty());
8390
8391        ctx.close().await;
8392        mock_ctx.close().await;
8393    }
8394
8395    #[tokio::test(flavor = "multi_thread")]
8396    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8397        let initial_source = "\
8398sketch(on = XY) {
8399  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8400  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8401  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8402  parallel([line1, line2, line3])
8403}
8404";
8405
8406        let program = Program::parse(initial_source).unwrap().0.unwrap();
8407
8408        let mut frontend = FrontendState::new();
8409
8410        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8411        let mock_ctx = ExecutorContext::new_mock(None).await;
8412        let version = Version(0);
8413
8414        frontend.hack_set_program(&ctx, program).await.unwrap();
8415        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8416        let sketch_id = sketch_object.id;
8417        let sketch = expect_sketch(sketch_object);
8418        let line3_id = *sketch.segments.get(8).unwrap();
8419
8420        let (src_delta, scene_delta) = frontend
8421            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8422            .await
8423            .unwrap();
8424        assert_eq!(
8425            src_delta.text.as_str(),
8426            "\
8427sketch(on = XY) {
8428  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8429  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8430  parallel([line1, line2])
8431}
8432"
8433        );
8434
8435        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8436        let sketch = expect_sketch(sketch_object);
8437        assert_eq!(sketch.constraints.len(), 1);
8438
8439        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8440        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8441            panic!("Expected constraint object");
8442        };
8443        let Constraint::Parallel(parallel) = constraint else {
8444            panic!("Expected parallel constraint");
8445        };
8446        assert_eq!(parallel.lines.len(), 2);
8447
8448        ctx.close().await;
8449        mock_ctx.close().await;
8450    }
8451
8452    #[tokio::test(flavor = "multi_thread")]
8453    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8454        let initial_source = "\
8455sketch(on = XY) {
8456  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8457  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8458  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8459  parallel([line1, line2, line3])
8460}
8461";
8462
8463        let program = Program::parse(initial_source).unwrap().0.unwrap();
8464
8465        let mut frontend = FrontendState::new();
8466
8467        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8468        let mock_ctx = ExecutorContext::new_mock(None).await;
8469        let version = Version(0);
8470
8471        frontend.hack_set_program(&ctx, program).await.unwrap();
8472        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8473        let sketch_id = sketch_object.id;
8474        let sketch = expect_sketch(sketch_object);
8475        let line2_id = *sketch.segments.get(5).unwrap();
8476        let line3_id = *sketch.segments.get(8).unwrap();
8477
8478        let (src_delta, scene_delta) = frontend
8479            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8480            .await
8481            .unwrap();
8482        assert_eq!(
8483            src_delta.text.as_str(),
8484            "\
8485sketch(on = XY) {
8486  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8487}
8488"
8489        );
8490
8491        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8492        let sketch = expect_sketch(sketch_object);
8493        assert!(sketch.constraints.is_empty());
8494
8495        ctx.close().await;
8496        mock_ctx.close().await;
8497    }
8498
8499    #[tokio::test(flavor = "multi_thread")]
8500    async fn test_delete_line_line_coincident_constraint() {
8501        let initial_source = "\
8502sketch(on = XY) {
8503  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8504  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8505  coincident([line1, line2])
8506}
8507";
8508
8509        let program = Program::parse(initial_source).unwrap().0.unwrap();
8510
8511        let mut frontend = FrontendState::new();
8512
8513        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8514        let mock_ctx = ExecutorContext::new_mock(None).await;
8515        let version = Version(0);
8516
8517        frontend.hack_set_program(&ctx, program).await.unwrap();
8518        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8519        let sketch_id = sketch_object.id;
8520        let sketch = expect_sketch(sketch_object);
8521
8522        let coincident_id = *sketch.constraints.first().unwrap();
8523
8524        let (src_delta, scene_delta) = frontend
8525            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8526            .await
8527            .unwrap();
8528        assert_eq!(
8529            src_delta.text.as_str(),
8530            "\
8531sketch(on = XY) {
8532  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8533  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8534}
8535"
8536        );
8537        assert_eq!(scene_delta.new_objects, vec![]);
8538        assert_eq!(scene_delta.new_graph.objects.len(), 8);
8539
8540        ctx.close().await;
8541        mock_ctx.close().await;
8542    }
8543
8544    #[tokio::test(flavor = "multi_thread")]
8545    async fn test_two_points_coincident() {
8546        let initial_source = "\
8547sketch(on = XY) {
8548  point1 = point(at = [var 1, var 2])
8549  point(at = [3, 4])
8550}
8551";
8552
8553        let program = Program::parse(initial_source).unwrap().0.unwrap();
8554
8555        let mut frontend = FrontendState::new();
8556
8557        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8558        let mock_ctx = ExecutorContext::new_mock(None).await;
8559        let version = Version(0);
8560
8561        frontend.hack_set_program(&ctx, program).await.unwrap();
8562        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8563        let sketch_id = sketch_object.id;
8564        let sketch = expect_sketch(sketch_object);
8565        let point0_id = *sketch.segments.first().unwrap();
8566        let point1_id = *sketch.segments.get(1).unwrap();
8567
8568        let constraint = Constraint::Coincident(Coincident {
8569            segments: vec![point0_id.into(), point1_id.into()],
8570        });
8571        let (src_delta, scene_delta) = frontend
8572            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8573            .await
8574            .unwrap();
8575        assert_eq!(
8576            src_delta.text.as_str(),
8577            "\
8578sketch(on = XY) {
8579  point1 = point(at = [var 1, var 2])
8580  point2 = point(at = [3, 4])
8581  coincident([point1, point2])
8582}
8583"
8584        );
8585        assert_eq!(
8586            scene_delta.new_graph.objects.len(),
8587            5,
8588            "{:#?}",
8589            scene_delta.new_graph.objects
8590        );
8591
8592        ctx.close().await;
8593        mock_ctx.close().await;
8594    }
8595
8596    #[tokio::test(flavor = "multi_thread")]
8597    async fn test_three_points_coincident() {
8598        let initial_source = "\
8599sketch(on = XY) {
8600  point1 = point(at = [var 1, var 2])
8601  point(at = [var 3, var 4])
8602  point(at = [var 5, var 6])
8603}
8604";
8605
8606        let program = Program::parse(initial_source).unwrap().0.unwrap();
8607
8608        let mut frontend = FrontendState::new();
8609
8610        let mock_ctx = ExecutorContext::new_mock(None).await;
8611        let version = Version(0);
8612
8613        frontend.program = program.clone();
8614        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8615        frontend.update_state_after_exec(outcome, true);
8616        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8617        let sketch_id = sketch_object.id;
8618        let sketch = expect_sketch(sketch_object);
8619        let segments = sketch
8620            .segments
8621            .iter()
8622            .take(3)
8623            .copied()
8624            .map(Into::into)
8625            .collect::<Vec<ConstraintSegment>>();
8626
8627        let constraint = Constraint::Coincident(Coincident {
8628            segments: segments.clone(),
8629        });
8630        let (src_delta, scene_delta) = frontend
8631            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8632            .await
8633            .unwrap();
8634        assert_eq!(
8635            src_delta.text.as_str(),
8636            "\
8637sketch(on = XY) {
8638  point1 = point(at = [var 1, var 2])
8639  point2 = point(at = [var 3, var 4])
8640  point3 = point(at = [var 5, var 6])
8641  coincident([point1, point2, point3])
8642}
8643"
8644        );
8645
8646        let constraint_object = scene_delta
8647            .new_graph
8648            .objects
8649            .iter()
8650            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8651            .unwrap();
8652
8653        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8654            panic!("expected a constraint object");
8655        };
8656
8657        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8658
8659        mock_ctx.close().await;
8660    }
8661
8662    #[tokio::test(flavor = "multi_thread")]
8663    async fn test_source_with_three_point_coincident_tracks_all_segments() {
8664        let initial_source = "\
8665sketch(on = XY) {
8666  point1 = point(at = [var 1, var 2])
8667  point2 = point(at = [var 3, var 4])
8668  point3 = point(at = [var 5, var 6])
8669  coincident([point1, point2, point3])
8670}
8671";
8672
8673        let program = Program::parse(initial_source).unwrap().0.unwrap();
8674
8675        let mut frontend = FrontendState::new();
8676
8677        let ctx = ExecutorContext::new_mock(None).await;
8678        frontend.program = program.clone();
8679        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8680        frontend.update_state_after_exec(outcome, true);
8681
8682        let constraint_object = frontend
8683            .scene_graph
8684            .objects
8685            .iter()
8686            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8687            .unwrap();
8688        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8689            panic!("expected a constraint object");
8690        };
8691
8692        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8693        let sketch = expect_sketch(sketch_object);
8694        let expected_segments = sketch
8695            .segments
8696            .iter()
8697            .take(3)
8698            .copied()
8699            .map(Into::into)
8700            .collect::<Vec<ConstraintSegment>>();
8701
8702        assert_eq!(
8703            constraint,
8704            &Constraint::Coincident(Coincident {
8705                segments: expected_segments,
8706            })
8707        );
8708
8709        ctx.close().await;
8710    }
8711
8712    #[tokio::test(flavor = "multi_thread")]
8713    async fn test_point_origin_coincident_preserves_order() {
8714        let initial_source = "\
8715sketch(on = XY) {
8716  point(at = [var 1, var 2])
8717}
8718";
8719
8720        for (origin_first, expected_source) in [
8721            (
8722                true,
8723                "\
8724sketch(on = XY) {
8725  point1 = point(at = [var 1, var 2])
8726  coincident([ORIGIN, point1])
8727}
8728",
8729            ),
8730            (
8731                false,
8732                "\
8733sketch(on = XY) {
8734  point1 = point(at = [var 1, var 2])
8735  coincident([point1, ORIGIN])
8736}
8737",
8738            ),
8739        ] {
8740            let program = Program::parse(initial_source).unwrap().0.unwrap();
8741
8742            let mut frontend = FrontendState::new();
8743
8744            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8745            let mock_ctx = ExecutorContext::new_mock(None).await;
8746            let version = Version(0);
8747
8748            frontend.hack_set_program(&ctx, program).await.unwrap();
8749            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8750            let sketch_id = sketch_object.id;
8751            let sketch = expect_sketch(sketch_object);
8752            let point_id = *sketch.segments.first().unwrap();
8753
8754            let segments = if origin_first {
8755                vec![ConstraintSegment::ORIGIN, point_id.into()]
8756            } else {
8757                vec![point_id.into(), ConstraintSegment::ORIGIN]
8758            };
8759            let constraint = Constraint::Coincident(Coincident {
8760                segments: segments.clone(),
8761            });
8762            let (src_delta, scene_delta) = frontend
8763                .add_constraint(&mock_ctx, version, sketch_id, constraint)
8764                .await
8765                .unwrap();
8766            assert_eq!(src_delta.text.as_str(), expected_source);
8767
8768            let constraint_object = scene_delta
8769                .new_graph
8770                .objects
8771                .iter()
8772                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8773                .unwrap();
8774
8775            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8776                panic!("expected a constraint object");
8777            };
8778
8779            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8780
8781            ctx.close().await;
8782            mock_ctx.close().await;
8783        }
8784    }
8785
8786    #[tokio::test(flavor = "multi_thread")]
8787    async fn test_coincident_of_line_end_points() {
8788        let initial_source = "\
8789sketch(on = XY) {
8790  line(start = [var 1, var 2], end = [var 3, var 4])
8791  line(start = [var 5, var 6], end = [var 7, var 8])
8792}
8793";
8794
8795        let program = Program::parse(initial_source).unwrap().0.unwrap();
8796
8797        let mut frontend = FrontendState::new();
8798
8799        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8800        let mock_ctx = ExecutorContext::new_mock(None).await;
8801        let version = Version(0);
8802
8803        frontend.hack_set_program(&ctx, program).await.unwrap();
8804        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8805        let sketch_id = sketch_object.id;
8806        let sketch = expect_sketch(sketch_object);
8807        let point0_id = *sketch.segments.get(1).unwrap();
8808        let point1_id = *sketch.segments.get(3).unwrap();
8809
8810        let constraint = Constraint::Coincident(Coincident {
8811            segments: vec![point0_id.into(), point1_id.into()],
8812        });
8813        let (src_delta, scene_delta) = frontend
8814            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8815            .await
8816            .unwrap();
8817        assert_eq!(
8818            src_delta.text.as_str(),
8819            "\
8820sketch(on = XY) {
8821  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8822  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8823  coincident([line1.end, line2.start])
8824}
8825"
8826        );
8827        assert_eq!(
8828            scene_delta.new_graph.objects.len(),
8829            9,
8830            "{:#?}",
8831            scene_delta.new_graph.objects
8832        );
8833
8834        ctx.close().await;
8835        mock_ctx.close().await;
8836    }
8837
8838    #[tokio::test(flavor = "multi_thread")]
8839    async fn test_coincident_of_line_point_and_circle_segment() {
8840        let initial_source = "\
8841sketch(on = XY) {
8842  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8843  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8844}
8845";
8846        let program = Program::parse(initial_source).unwrap().0.unwrap();
8847        let mut frontend = FrontendState::new();
8848
8849        let mock_ctx = ExecutorContext::new_mock(None).await;
8850        let version = Version(0);
8851
8852        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8853        frontend.program = program;
8854        frontend.update_state_after_exec(outcome, true);
8855        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8856        let sketch_id = sketch_object.id;
8857        let sketch = expect_sketch(sketch_object);
8858
8859        let circle_id = sketch
8860            .segments
8861            .iter()
8862            .copied()
8863            .find(|seg_id| {
8864                matches!(
8865                    &frontend.scene_graph.objects[seg_id.0].kind,
8866                    ObjectKind::Segment {
8867                        segment: Segment::Circle(_)
8868                    }
8869                )
8870            })
8871            .expect("Expected a circle segment in sketch");
8872        let line_id = sketch
8873            .segments
8874            .iter()
8875            .copied()
8876            .find(|seg_id| {
8877                matches!(
8878                    &frontend.scene_graph.objects[seg_id.0].kind,
8879                    ObjectKind::Segment {
8880                        segment: Segment::Line(_)
8881                    }
8882                )
8883            })
8884            .expect("Expected a line segment in sketch");
8885
8886        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8887            ObjectKind::Segment {
8888                segment: Segment::Line(line),
8889            } => line.start,
8890            _ => panic!("Expected line segment object"),
8891        };
8892
8893        let constraint = Constraint::Coincident(Coincident {
8894            segments: vec![line_start_point_id.into(), circle_id.into()],
8895        });
8896        let (src_delta, _scene_delta) = frontend
8897            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8898            .await
8899            .unwrap();
8900        assert_eq!(
8901            src_delta.text.as_str(),
8902            "\
8903sketch(on = XY) {
8904  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8905  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8906  coincident([line1.start, circle1])
8907}
8908"
8909        );
8910
8911        mock_ctx.close().await;
8912    }
8913
8914    #[tokio::test(flavor = "multi_thread")]
8915    async fn test_invalid_coincident_arc_and_line_preserves_state() {
8916        // Test that attempting an invalid coincident constraint (arc and line)
8917        // doesn't corrupt the state, allowing subsequent operations to work.
8918        // This test verifies the transactional fix in add_constraint that prevents
8919        // state corruption when invalid constraints are attempted.
8920        // Example: coincident constraint between an arc segment and a straight line segment
8921        // is geometrically invalid and should fail, but state should remain intact.
8922        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
8923        let program = Program::empty();
8924
8925        let mut frontend = FrontendState::new();
8926        frontend.program = program;
8927
8928        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8929        let mock_ctx = ExecutorContext::new_mock(None).await;
8930        let version = Version(0);
8931
8932        let sketch_args = SketchCtor {
8933            on: Plane::Default(PlaneName::Xy),
8934        };
8935        let (_src_delta, _scene_delta, sketch_id) = frontend
8936            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8937            .await
8938            .unwrap();
8939
8940        // Add an arc segment
8941        let arc_ctor = ArcCtor {
8942            start: Point2d {
8943                x: Expr::Var(Number {
8944                    value: 0.0,
8945                    units: NumericSuffix::Mm,
8946                }),
8947                y: Expr::Var(Number {
8948                    value: 0.0,
8949                    units: NumericSuffix::Mm,
8950                }),
8951            },
8952            end: Point2d {
8953                x: Expr::Var(Number {
8954                    value: 10.0,
8955                    units: NumericSuffix::Mm,
8956                }),
8957                y: Expr::Var(Number {
8958                    value: 10.0,
8959                    units: NumericSuffix::Mm,
8960                }),
8961            },
8962            center: Point2d {
8963                x: Expr::Var(Number {
8964                    value: 10.0,
8965                    units: NumericSuffix::Mm,
8966                }),
8967                y: Expr::Var(Number {
8968                    value: 0.0,
8969                    units: NumericSuffix::Mm,
8970                }),
8971            },
8972            construction: None,
8973        };
8974        let (_src_delta, scene_delta) = frontend
8975            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8976            .await
8977            .unwrap();
8978        // The arc is the last object in new_objects (after the 3 points: start, end, center)
8979        let arc_id = *scene_delta.new_objects.last().unwrap();
8980
8981        // Add a line segment
8982        let line_ctor = LineCtor {
8983            start: Point2d {
8984                x: Expr::Var(Number {
8985                    value: 20.0,
8986                    units: NumericSuffix::Mm,
8987                }),
8988                y: Expr::Var(Number {
8989                    value: 0.0,
8990                    units: NumericSuffix::Mm,
8991                }),
8992            },
8993            end: Point2d {
8994                x: Expr::Var(Number {
8995                    value: 30.0,
8996                    units: NumericSuffix::Mm,
8997                }),
8998                y: Expr::Var(Number {
8999                    value: 10.0,
9000                    units: NumericSuffix::Mm,
9001                }),
9002            },
9003            construction: None,
9004        };
9005        let (_src_delta, scene_delta) = frontend
9006            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9007            .await
9008            .unwrap();
9009        // The line is the last object in new_objects (after the 2 points: start, end)
9010        let line_id = *scene_delta.new_objects.last().unwrap();
9011
9012        // Attempt to add an invalid coincident constraint between arc and line
9013        // This should fail during execution, but state should remain intact
9014        let constraint = Constraint::Coincident(Coincident {
9015            segments: vec![arc_id.into(), line_id.into()],
9016        });
9017        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9018
9019        // The constraint addition should fail (invalid constraint)
9020        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9021
9022        // Verify state is not corrupted by checking that we can still access the scene graph
9023        // and that the original segments are still present with their source ranges
9024        let sketch_object_after =
9025            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9026        let sketch_after = expect_sketch(sketch_object_after);
9027
9028        // Verify both segments are still in the sketch
9029        assert!(
9030            sketch_after.segments.contains(&arc_id),
9031            "Arc segment should still exist after failed constraint"
9032        );
9033        assert!(
9034            sketch_after.segments.contains(&line_id),
9035            "Line segment should still exist after failed constraint"
9036        );
9037
9038        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
9039        let arc_obj = frontend
9040            .scene_graph
9041            .objects
9042            .get(arc_id.0)
9043            .expect("Arc object should still be accessible");
9044        let line_obj = frontend
9045            .scene_graph
9046            .objects
9047            .get(line_id.0)
9048            .expect("Line object should still be accessible");
9049
9050        // Verify source ranges are still valid (not corrupted)
9051        // Just verify that the objects are still accessible and have the expected types
9052        match &arc_obj.kind {
9053            ObjectKind::Segment {
9054                segment: Segment::Arc(_),
9055            } => {}
9056            _ => panic!("Arc object should still be an arc segment"),
9057        }
9058        match &line_obj.kind {
9059            ObjectKind::Segment {
9060                segment: Segment::Line(_),
9061            } => {}
9062            _ => panic!("Line object should still be a line segment"),
9063        }
9064
9065        ctx.close().await;
9066        mock_ctx.close().await;
9067    }
9068
9069    #[tokio::test(flavor = "multi_thread")]
9070    async fn test_distance_two_points() {
9071        let initial_source = "\
9072sketch(on = XY) {
9073  point(at = [var 1, var 2])
9074  point(at = [var 3, var 4])
9075}
9076";
9077
9078        let program = Program::parse(initial_source).unwrap().0.unwrap();
9079
9080        let mut frontend = FrontendState::new();
9081
9082        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9083        let mock_ctx = ExecutorContext::new_mock(None).await;
9084        let version = Version(0);
9085
9086        frontend.hack_set_program(&ctx, program).await.unwrap();
9087        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9088        let sketch_id = sketch_object.id;
9089        let sketch = expect_sketch(sketch_object);
9090        let point0_id = *sketch.segments.first().unwrap();
9091        let point1_id = *sketch.segments.get(1).unwrap();
9092
9093        let constraint = Constraint::Distance(Distance {
9094            points: vec![point0_id.into(), point1_id.into()],
9095            distance: Number {
9096                value: 2.0,
9097                units: NumericSuffix::Mm,
9098            },
9099            label_position: None,
9100            source: Default::default(),
9101        });
9102        let (src_delta, scene_delta) = frontend
9103            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9104            .await
9105            .unwrap();
9106        assert_eq!(
9107            src_delta.text.as_str(),
9108            // The lack indentation is a formatter bug.
9109            "\
9110sketch(on = XY) {
9111  point1 = point(at = [var 1, var 2])
9112  point2 = point(at = [var 3, var 4])
9113  distance([point1, point2]) == 2mm
9114}
9115"
9116        );
9117        assert_eq!(
9118            scene_delta.new_graph.objects.len(),
9119            5,
9120            "{:#?}",
9121            scene_delta.new_graph.objects
9122        );
9123
9124        ctx.close().await;
9125        mock_ctx.close().await;
9126    }
9127
9128    #[tokio::test(flavor = "multi_thread")]
9129    async fn test_distance_two_points_with_label() {
9130        let initial_source = "\
9131sketch(on = XY) {
9132  point(at = [var 1, var 2])
9133  point(at = [var 3, var 4])
9134}
9135";
9136
9137        let program = Program::parse(initial_source).unwrap().0.unwrap();
9138
9139        let mut frontend = FrontendState::new();
9140
9141        let mock_ctx = ExecutorContext::new_mock(None).await;
9142        let version = Version(0);
9143
9144        frontend.program = program.clone();
9145        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9146        frontend.update_state_after_exec(outcome, true);
9147        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9148        let sketch_id = sketch_object.id;
9149        let sketch = expect_sketch(sketch_object);
9150        let point0_id = *sketch.segments.first().unwrap();
9151        let point1_id = *sketch.segments.get(1).unwrap();
9152
9153        let label_position = Point2d {
9154            x: Number {
9155                value: 10.0,
9156                units: NumericSuffix::Mm,
9157            },
9158            y: Number {
9159                value: 11.0,
9160                units: NumericSuffix::Mm,
9161            },
9162        };
9163        let constraint = Constraint::Distance(Distance {
9164            points: vec![point0_id.into(), point1_id.into()],
9165            distance: Number {
9166                value: 2.0,
9167                units: NumericSuffix::Mm,
9168            },
9169            label_position: Some(label_position.clone()),
9170            source: Default::default(),
9171        });
9172        let (src_delta, scene_delta) = frontend
9173            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9174            .await
9175            .unwrap();
9176        assert_eq!(
9177            src_delta.text.as_str(),
9178            "\
9179sketch(on = XY) {
9180  point1 = point(at = [var 1, var 2])
9181  point2 = point(at = [var 3, var 4])
9182  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9183}
9184"
9185        );
9186
9187        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9188        let sketch = expect_sketch(sketch_object);
9189        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9190        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9191            panic!("Expected constraint object");
9192        };
9193        let Constraint::Distance(distance) = constraint else {
9194            panic!("Expected distance constraint");
9195        };
9196        assert_eq!(distance.label_position, Some(label_position));
9197
9198        mock_ctx.close().await;
9199    }
9200
9201    #[tokio::test(flavor = "multi_thread")]
9202    async fn test_edit_distance_constraint_label_position() {
9203        let initial_source = "\
9204sketch(on = XY) {
9205  point(at = [var 1, var 2])
9206  point(at = [var 3, var 2])
9207}
9208";
9209
9210        let program = Program::parse(initial_source).unwrap().0.unwrap();
9211
9212        let mut frontend = FrontendState::new();
9213
9214        let mock_ctx = ExecutorContext::new_mock(None).await;
9215        let version = Version(0);
9216
9217        frontend.program = program.clone();
9218        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9219        frontend.update_state_after_exec(outcome, true);
9220        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9221        let sketch_id = sketch_object.id;
9222        let sketch = expect_sketch(sketch_object);
9223        let point0_id = *sketch.segments.first().unwrap();
9224        let point1_id = *sketch.segments.get(1).unwrap();
9225
9226        let constraint = Constraint::Distance(Distance {
9227            points: vec![point0_id.into(), point1_id.into()],
9228            distance: Number {
9229                value: 2.0,
9230                units: NumericSuffix::Mm,
9231            },
9232            label_position: None,
9233            source: Default::default(),
9234        });
9235        let (_, scene_delta) = frontend
9236            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9237            .await
9238            .unwrap();
9239        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9240        let sketch = expect_sketch(sketch_object);
9241        let constraint_id = sketch.constraints[0];
9242        let label_position = Point2d {
9243            x: Number {
9244                value: 10.0,
9245                units: NumericSuffix::Mm,
9246            },
9247            y: Number {
9248                value: 11.0,
9249                units: NumericSuffix::Mm,
9250            },
9251        };
9252
9253        let (src_delta, scene_delta) = frontend
9254            .edit_distance_constraint_label_position(
9255                &mock_ctx,
9256                version,
9257                sketch_id,
9258                constraint_id,
9259                label_position.clone(),
9260                vec![],
9261            )
9262            .await
9263            .unwrap();
9264        assert_eq!(
9265            src_delta.text.as_str(),
9266            "\
9267sketch(on = XY) {
9268  point1 = point(at = [var 1mm, var 2mm])
9269  point2 = point(at = [var 3mm, var 2mm])
9270  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9271}
9272"
9273        );
9274
9275        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9276        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9277            panic!("Expected constraint object");
9278        };
9279        let Constraint::Distance(distance) = constraint else {
9280            panic!("Expected distance constraint");
9281        };
9282        assert_eq!(distance.label_position, Some(label_position));
9283
9284        mock_ctx.close().await;
9285    }
9286
9287    #[tokio::test(flavor = "multi_thread")]
9288    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9289        let initial_source = "\
9290sketch(on = XY) {
9291  point1 = point(at = [var 0mm, var 0mm])
9292  point2 = point(at = [var 10mm, var 0mm])
9293  distance([point1, point2]) == 5mm
9294}
9295";
9296
9297        let program = Program::parse(initial_source).unwrap().0.unwrap();
9298        let mut frontend = FrontendState::new();
9299        let mock_ctx = ExecutorContext::new_mock(None).await;
9300        let version = Version(0);
9301
9302        frontend.program = program.clone();
9303        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9304        frontend.update_state_after_exec(outcome, true);
9305        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9306        let sketch_id = sketch_object.id;
9307        let sketch = expect_sketch(sketch_object);
9308        let point0_id = sketch.segments[0];
9309        let point1_id = sketch.segments[1];
9310        let constraint_id = sketch.constraints[0];
9311
9312        let edited_segments = vec![ExistingSegmentCtor {
9313            id: point0_id,
9314            ctor: SegmentCtor::Point(PointCtor {
9315                position: Point2d {
9316                    x: Expr::Var(Number {
9317                        value: 2.0,
9318                        units: NumericSuffix::Mm,
9319                    }),
9320                    y: Expr::Var(Number {
9321                        value: 1.0,
9322                        units: NumericSuffix::Mm,
9323                    }),
9324                },
9325            }),
9326        }];
9327        let (_, scene_delta) = frontend
9328            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9329            .await
9330            .unwrap();
9331        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9332        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9333
9334        let label_position = Point2d {
9335            x: Number {
9336                value: 3.0,
9337                units: NumericSuffix::Mm,
9338            },
9339            y: Number {
9340                value: 4.0,
9341                units: NumericSuffix::Mm,
9342            },
9343        };
9344        let (_, scene_delta) = frontend
9345            .edit_distance_constraint_label_position(
9346                &mock_ctx,
9347                version,
9348                sketch_id,
9349                constraint_id,
9350                label_position,
9351                vec![point0_id],
9352            )
9353            .await
9354            .unwrap();
9355
9356        assert_point_position_close(
9357            point_position(&scene_delta.new_graph, point0_id),
9358            point0_after_segment_edit,
9359        );
9360        assert_point_position_close(
9361            point_position(&scene_delta.new_graph, point1_id),
9362            point1_after_segment_edit,
9363        );
9364
9365        mock_ctx.close().await;
9366    }
9367
9368    #[tokio::test(flavor = "multi_thread")]
9369    async fn test_horizontal_distance_two_points() {
9370        let initial_source = "\
9371sketch(on = XY) {
9372  point(at = [var 1, var 2])
9373  point(at = [var 3, var 4])
9374}
9375";
9376
9377        let program = Program::parse(initial_source).unwrap().0.unwrap();
9378
9379        let mut frontend = FrontendState::new();
9380
9381        let mock_ctx = ExecutorContext::new_mock(None).await;
9382        let version = Version(0);
9383
9384        frontend.program = program.clone();
9385        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9386        frontend.update_state_after_exec(outcome, true);
9387        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9388        let sketch_id = sketch_object.id;
9389        let sketch = expect_sketch(sketch_object);
9390        let point0_id = *sketch.segments.first().unwrap();
9391        let point1_id = *sketch.segments.get(1).unwrap();
9392        let label_position = Point2d {
9393            x: Number {
9394                value: 10.0,
9395                units: NumericSuffix::Mm,
9396            },
9397            y: Number {
9398                value: 11.0,
9399                units: NumericSuffix::Mm,
9400            },
9401        };
9402
9403        let constraint = Constraint::HorizontalDistance(Distance {
9404            points: vec![point0_id.into(), point1_id.into()],
9405            distance: Number {
9406                value: 2.0,
9407                units: NumericSuffix::Mm,
9408            },
9409            label_position: Some(label_position.clone()),
9410            source: Default::default(),
9411        });
9412        let (src_delta, scene_delta) = frontend
9413            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9414            .await
9415            .unwrap();
9416        assert_eq!(
9417            src_delta.text.as_str(),
9418            // The lack indentation is a formatter bug.
9419            "\
9420sketch(on = XY) {
9421  point1 = point(at = [var 1, var 2])
9422  point2 = point(at = [var 3, var 4])
9423  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9424}
9425"
9426        );
9427        assert_eq!(
9428            scene_delta.new_graph.objects.len(),
9429            5,
9430            "{:#?}",
9431            scene_delta.new_graph.objects
9432        );
9433        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9434        let sketch = expect_sketch(sketch_object);
9435        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9436        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9437            panic!("Expected constraint object");
9438        };
9439        let Constraint::HorizontalDistance(distance) = constraint else {
9440            panic!("Expected horizontal distance constraint");
9441        };
9442        assert_eq!(distance.label_position, Some(label_position));
9443
9444        mock_ctx.close().await;
9445    }
9446
9447    #[tokio::test(flavor = "multi_thread")]
9448    async fn test_radius_single_arc_segment() {
9449        let initial_source = "\
9450sketch(on = XY) {
9451  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9452}
9453";
9454
9455        let program = Program::parse(initial_source).unwrap().0.unwrap();
9456
9457        let mut frontend = FrontendState::new();
9458
9459        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9460        let mock_ctx = ExecutorContext::new_mock(None).await;
9461        let version = Version(0);
9462
9463        frontend.hack_set_program(&ctx, program).await.unwrap();
9464        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9465        let sketch_id = sketch_object.id;
9466        let sketch = expect_sketch(sketch_object);
9467        // Find the arc segment (not the points)
9468        let arc_id = sketch
9469            .segments
9470            .iter()
9471            .find(|&seg_id| {
9472                let obj = frontend.scene_graph.objects.get(seg_id.0);
9473                matches!(
9474                    obj.map(|o| &o.kind),
9475                    Some(ObjectKind::Segment {
9476                        segment: Segment::Arc(_)
9477                    })
9478                )
9479            })
9480            .unwrap();
9481
9482        let constraint = Constraint::Radius(Radius {
9483            arc: *arc_id,
9484            radius: Number {
9485                value: 5.0,
9486                units: NumericSuffix::Mm,
9487            },
9488            label_position: None,
9489            source: Default::default(),
9490        });
9491        let (src_delta, scene_delta) = frontend
9492            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9493            .await
9494            .unwrap();
9495        assert_eq!(
9496            src_delta.text.as_str(),
9497            // The lack indentation is a formatter bug.
9498            "\
9499sketch(on = XY) {
9500  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9501  radius(arc1) == 5mm
9502}
9503"
9504        );
9505        assert_eq!(
9506            scene_delta.new_graph.objects.len(),
9507            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
9508            "{:#?}",
9509            scene_delta.new_graph.objects
9510        );
9511
9512        ctx.close().await;
9513        mock_ctx.close().await;
9514    }
9515
9516    #[tokio::test(flavor = "multi_thread")]
9517    async fn test_radius_single_arc_segment_with_label_position() {
9518        let initial_source = "\
9519sketch(on = XY) {
9520  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9521}
9522";
9523
9524        let program = Program::parse(initial_source).unwrap().0.unwrap();
9525        let mut frontend = FrontendState::new();
9526        let mock_ctx = ExecutorContext::new_mock(None).await;
9527        let version = Version(0);
9528
9529        frontend.program = program.clone();
9530        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9531        frontend.update_state_after_exec(outcome, true);
9532        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9533        let sketch_id = sketch_object.id;
9534        let sketch = expect_sketch(sketch_object);
9535        let arc_id = sketch
9536            .segments
9537            .iter()
9538            .find(|&seg_id| {
9539                let obj = frontend.scene_graph.objects.get(seg_id.0);
9540                matches!(
9541                    obj.map(|o| &o.kind),
9542                    Some(ObjectKind::Segment {
9543                        segment: Segment::Arc(_)
9544                    })
9545                )
9546            })
9547            .unwrap();
9548
9549        let label_position = Point2d {
9550            x: Number {
9551                value: 10.0,
9552                units: NumericSuffix::Mm,
9553            },
9554            y: Number {
9555                value: 11.0,
9556                units: NumericSuffix::Mm,
9557            },
9558        };
9559        let constraint = Constraint::Radius(Radius {
9560            arc: *arc_id,
9561            radius: Number {
9562                value: 5.0,
9563                units: NumericSuffix::Mm,
9564            },
9565            label_position: Some(label_position.clone()),
9566            source: Default::default(),
9567        });
9568        let (src_delta, scene_delta) = frontend
9569            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9570            .await
9571            .unwrap();
9572        assert_eq!(
9573            src_delta.text.as_str(),
9574            "\
9575sketch(on = XY) {
9576  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9577  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9578}
9579"
9580        );
9581
9582        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9583        let sketch = expect_sketch(sketch_object);
9584        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9585        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9586            panic!("Expected constraint object");
9587        };
9588        let Constraint::Radius(radius) = constraint else {
9589            panic!("Expected radius constraint");
9590        };
9591        assert_eq!(radius.label_position, Some(label_position));
9592
9593        mock_ctx.close().await;
9594    }
9595
9596    #[tokio::test(flavor = "multi_thread")]
9597    async fn test_edit_radius_constraint_label_position() {
9598        let initial_source = "\
9599sketch(on = XY) {
9600  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9601  radius(arc1) == 5mm
9602}
9603";
9604
9605        let program = Program::parse(initial_source).unwrap().0.unwrap();
9606        let mut frontend = FrontendState::new();
9607        let mock_ctx = ExecutorContext::new_mock(None).await;
9608        let version = Version(0);
9609
9610        frontend.program = program.clone();
9611        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9612        frontend.update_state_after_exec(outcome, true);
9613        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9614        let sketch_id = sketch_object.id;
9615        let sketch = expect_sketch(sketch_object);
9616        let constraint_id = sketch.constraints[0];
9617        let label_position = Point2d {
9618            x: Number {
9619                value: 10.0,
9620                units: NumericSuffix::Mm,
9621            },
9622            y: Number {
9623                value: 11.0,
9624                units: NumericSuffix::Mm,
9625            },
9626        };
9627
9628        let (src_delta, scene_delta) = frontend
9629            .edit_distance_constraint_label_position(
9630                &mock_ctx,
9631                version,
9632                sketch_id,
9633                constraint_id,
9634                label_position.clone(),
9635                vec![],
9636            )
9637            .await
9638            .unwrap();
9639        assert_eq!(
9640            src_delta.text.as_str(),
9641            "\
9642sketch(on = XY) {
9643  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9644  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9645}
9646"
9647        );
9648
9649        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9650        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9651            panic!("Expected constraint object");
9652        };
9653        let Constraint::Radius(radius) = constraint else {
9654            panic!("Expected radius constraint");
9655        };
9656        assert_eq!(radius.label_position, Some(label_position));
9657
9658        mock_ctx.close().await;
9659    }
9660
9661    #[tokio::test(flavor = "multi_thread")]
9662    async fn test_vertical_distance_two_points() {
9663        let initial_source = "\
9664sketch(on = XY) {
9665  point(at = [var 1, var 2])
9666  point(at = [var 3, var 4])
9667}
9668";
9669
9670        let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672        let mut frontend = FrontendState::new();
9673
9674        let mock_ctx = ExecutorContext::new_mock(None).await;
9675        let version = Version(0);
9676
9677        frontend.program = program.clone();
9678        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9679        frontend.update_state_after_exec(outcome, true);
9680        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9681        let sketch_id = sketch_object.id;
9682        let sketch = expect_sketch(sketch_object);
9683        let point0_id = *sketch.segments.first().unwrap();
9684        let point1_id = *sketch.segments.get(1).unwrap();
9685        let label_position = Point2d {
9686            x: Number {
9687                value: 10.0,
9688                units: NumericSuffix::Mm,
9689            },
9690            y: Number {
9691                value: 11.0,
9692                units: NumericSuffix::Mm,
9693            },
9694        };
9695
9696        let constraint = Constraint::VerticalDistance(Distance {
9697            points: vec![point0_id.into(), point1_id.into()],
9698            distance: Number {
9699                value: 2.0,
9700                units: NumericSuffix::Mm,
9701            },
9702            label_position: Some(label_position.clone()),
9703            source: Default::default(),
9704        });
9705        let (src_delta, scene_delta) = frontend
9706            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9707            .await
9708            .unwrap();
9709        assert_eq!(
9710            src_delta.text.as_str(),
9711            // The lack indentation is a formatter bug.
9712            "\
9713sketch(on = XY) {
9714  point1 = point(at = [var 1, var 2])
9715  point2 = point(at = [var 3, var 4])
9716  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9717}
9718"
9719        );
9720        assert_eq!(
9721            scene_delta.new_graph.objects.len(),
9722            5,
9723            "{:#?}",
9724            scene_delta.new_graph.objects
9725        );
9726        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9727        let sketch = expect_sketch(sketch_object);
9728        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9729        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9730            panic!("Expected constraint object");
9731        };
9732        let Constraint::VerticalDistance(distance) = constraint else {
9733            panic!("Expected vertical distance constraint");
9734        };
9735        assert_eq!(distance.label_position, Some(label_position));
9736
9737        mock_ctx.close().await;
9738    }
9739
9740    #[tokio::test(flavor = "multi_thread")]
9741    async fn test_add_fixed_standalone_point() {
9742        let initial_source = "\
9743sketch(on = XY) {
9744  point(at = [var 1, var 2])
9745}
9746";
9747
9748        let program = Program::parse(initial_source).unwrap().0.unwrap();
9749
9750        let mut frontend = FrontendState::new();
9751
9752        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9753        let mock_ctx = ExecutorContext::new_mock(None).await;
9754        let version = Version(0);
9755
9756        frontend.hack_set_program(&ctx, program).await.unwrap();
9757        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9758        let sketch_id = sketch_object.id;
9759        let sketch = expect_sketch(sketch_object);
9760        let point_id = *sketch.segments.first().unwrap();
9761
9762        let (src_delta, scene_delta) = frontend
9763            .add_constraint(
9764                &mock_ctx,
9765                version,
9766                sketch_id,
9767                Constraint::Fixed(Fixed {
9768                    points: vec![FixedPoint {
9769                        point: point_id,
9770                        position: Point2d {
9771                            x: Number {
9772                                value: 2.0,
9773                                units: NumericSuffix::Mm,
9774                            },
9775                            y: Number {
9776                                value: 3.0,
9777                                units: NumericSuffix::Mm,
9778                            },
9779                        },
9780                    }],
9781                }),
9782            )
9783            .await
9784            .unwrap();
9785        assert_eq!(
9786            src_delta.text.as_str(),
9787            "\
9788sketch(on = XY) {
9789  point1 = point(at = [var 1, var 2])
9790  fixed([point1, [2mm, 3mm]])
9791}
9792"
9793        );
9794        assert_eq!(
9795            scene_delta.new_graph.objects.len(),
9796            4,
9797            "{:#?}",
9798            scene_delta.new_graph.objects
9799        );
9800
9801        ctx.close().await;
9802        mock_ctx.close().await;
9803    }
9804
9805    #[tokio::test(flavor = "multi_thread")]
9806    async fn test_add_fixed_multiple_points() {
9807        let initial_source = "\
9808sketch(on = XY) {
9809  point(at = [var 1, var 2])
9810  point(at = [var 3, var 4])
9811}
9812";
9813
9814        let program = Program::parse(initial_source).unwrap().0.unwrap();
9815
9816        let mut frontend = FrontendState::new();
9817
9818        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9819        let mock_ctx = ExecutorContext::new_mock(None).await;
9820        let version = Version(0);
9821
9822        frontend.hack_set_program(&ctx, program).await.unwrap();
9823        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9824        let sketch_id = sketch_object.id;
9825        let sketch = expect_sketch(sketch_object);
9826        let point0_id = *sketch.segments.first().unwrap();
9827        let point1_id = *sketch.segments.get(1).unwrap();
9828
9829        let (src_delta, scene_delta) = frontend
9830            .add_constraint(
9831                &mock_ctx,
9832                version,
9833                sketch_id,
9834                Constraint::Fixed(Fixed {
9835                    points: vec![
9836                        FixedPoint {
9837                            point: point0_id,
9838                            position: Point2d {
9839                                x: Number {
9840                                    value: 2.0,
9841                                    units: NumericSuffix::Mm,
9842                                },
9843                                y: Number {
9844                                    value: 3.0,
9845                                    units: NumericSuffix::Mm,
9846                                },
9847                            },
9848                        },
9849                        FixedPoint {
9850                            point: point1_id,
9851                            position: Point2d {
9852                                x: Number {
9853                                    value: 4.0,
9854                                    units: NumericSuffix::Mm,
9855                                },
9856                                y: Number {
9857                                    value: 5.0,
9858                                    units: NumericSuffix::Mm,
9859                                },
9860                            },
9861                        },
9862                    ],
9863                }),
9864            )
9865            .await
9866            .unwrap();
9867        assert_eq!(
9868            src_delta.text.as_str(),
9869            "\
9870sketch(on = XY) {
9871  point1 = point(at = [var 1, var 2])
9872  point2 = point(at = [var 3, var 4])
9873  fixed([point1, [2mm, 3mm]])
9874  fixed([point2, [4mm, 5mm]])
9875}
9876"
9877        );
9878        assert_eq!(
9879            scene_delta.new_graph.objects.len(),
9880            6,
9881            "{:#?}",
9882            scene_delta.new_graph.objects
9883        );
9884
9885        ctx.close().await;
9886        mock_ctx.close().await;
9887    }
9888
9889    #[tokio::test(flavor = "multi_thread")]
9890    async fn test_add_fixed_owned_point() {
9891        let initial_source = "\
9892sketch(on = XY) {
9893  line(start = [var 1, var 2], end = [var 3, var 4])
9894}
9895";
9896
9897        let program = Program::parse(initial_source).unwrap().0.unwrap();
9898
9899        let mut frontend = FrontendState::new();
9900
9901        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9902        let mock_ctx = ExecutorContext::new_mock(None).await;
9903        let version = Version(0);
9904
9905        frontend.hack_set_program(&ctx, program).await.unwrap();
9906        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9907        let sketch_id = sketch_object.id;
9908        let sketch = expect_sketch(sketch_object);
9909        let line_start_id = *sketch.segments.first().unwrap();
9910
9911        let (src_delta, scene_delta) = frontend
9912            .add_constraint(
9913                &mock_ctx,
9914                version,
9915                sketch_id,
9916                Constraint::Fixed(Fixed {
9917                    points: vec![FixedPoint {
9918                        point: line_start_id,
9919                        position: Point2d {
9920                            x: Number {
9921                                value: 2.0,
9922                                units: NumericSuffix::Mm,
9923                            },
9924                            y: Number {
9925                                value: 3.0,
9926                                units: NumericSuffix::Mm,
9927                            },
9928                        },
9929                    }],
9930                }),
9931            )
9932            .await
9933            .unwrap();
9934        assert_eq!(
9935            src_delta.text.as_str(),
9936            "\
9937sketch(on = XY) {
9938  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9939  fixed([line1.start, [2mm, 3mm]])
9940}
9941"
9942        );
9943        assert_eq!(
9944            scene_delta.new_graph.objects.len(),
9945            6,
9946            "{:#?}",
9947            scene_delta.new_graph.objects
9948        );
9949
9950        ctx.close().await;
9951        mock_ctx.close().await;
9952    }
9953
9954    #[tokio::test(flavor = "multi_thread")]
9955    async fn test_radius_error_cases() {
9956        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9957        let mock_ctx = ExecutorContext::new_mock(None).await;
9958        let version = Version(0);
9959
9960        // Test: Single point should error
9961        let initial_source_point = "\
9962sketch(on = XY) {
9963  point(at = [var 1, var 2])
9964}
9965";
9966        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9967        let mut frontend_point = FrontendState::new();
9968        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9969        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9970        let sketch_id_point = sketch_object_point.id;
9971        let sketch_point = expect_sketch(sketch_object_point);
9972        let point_id = *sketch_point.segments.first().unwrap();
9973
9974        let constraint_point = Constraint::Radius(Radius {
9975            arc: point_id,
9976            radius: Number {
9977                value: 5.0,
9978                units: NumericSuffix::Mm,
9979            },
9980            label_position: None,
9981            source: Default::default(),
9982        });
9983        let result_point = frontend_point
9984            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9985            .await;
9986        assert!(result_point.is_err(), "Single point should error for radius");
9987
9988        // Test: Single line segment should error (only arc segments supported)
9989        let initial_source_line = "\
9990sketch(on = XY) {
9991  line(start = [var 1, var 2], end = [var 3, var 4])
9992}
9993";
9994        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9995        let mut frontend_line = FrontendState::new();
9996        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
9997        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
9998        let sketch_id_line = sketch_object_line.id;
9999        let sketch_line = expect_sketch(sketch_object_line);
10000        let line_id = *sketch_line.segments.first().unwrap();
10001
10002        let constraint_line = Constraint::Radius(Radius {
10003            arc: line_id,
10004            radius: Number {
10005                value: 5.0,
10006                units: NumericSuffix::Mm,
10007            },
10008            label_position: None,
10009            source: Default::default(),
10010        });
10011        let result_line = frontend_line
10012            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10013            .await;
10014        assert!(result_line.is_err(), "Single line segment should error for radius");
10015
10016        ctx.close().await;
10017        mock_ctx.close().await;
10018    }
10019
10020    #[tokio::test(flavor = "multi_thread")]
10021    async fn test_diameter_single_arc_segment() {
10022        let initial_source = "\
10023sketch(on = XY) {
10024  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10025}
10026";
10027
10028        let program = Program::parse(initial_source).unwrap().0.unwrap();
10029
10030        let mut frontend = FrontendState::new();
10031
10032        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10033        let mock_ctx = ExecutorContext::new_mock(None).await;
10034        let version = Version(0);
10035
10036        frontend.hack_set_program(&ctx, program).await.unwrap();
10037        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10038        let sketch_id = sketch_object.id;
10039        let sketch = expect_sketch(sketch_object);
10040        // Find the arc segment (not the points)
10041        let arc_id = sketch
10042            .segments
10043            .iter()
10044            .find(|&seg_id| {
10045                let obj = frontend.scene_graph.objects.get(seg_id.0);
10046                matches!(
10047                    obj.map(|o| &o.kind),
10048                    Some(ObjectKind::Segment {
10049                        segment: Segment::Arc(_)
10050                    })
10051                )
10052            })
10053            .unwrap();
10054
10055        let constraint = Constraint::Diameter(Diameter {
10056            arc: *arc_id,
10057            diameter: Number {
10058                value: 10.0,
10059                units: NumericSuffix::Mm,
10060            },
10061            label_position: None,
10062            source: Default::default(),
10063        });
10064        let (src_delta, scene_delta) = frontend
10065            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10066            .await
10067            .unwrap();
10068        assert_eq!(
10069            src_delta.text.as_str(),
10070            // The lack indentation is a formatter bug.
10071            "\
10072sketch(on = XY) {
10073  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10074  diameter(arc1) == 10mm
10075}
10076"
10077        );
10078        assert_eq!(
10079            scene_delta.new_graph.objects.len(),
10080            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10081            "{:#?}",
10082            scene_delta.new_graph.objects
10083        );
10084
10085        ctx.close().await;
10086        mock_ctx.close().await;
10087    }
10088
10089    #[tokio::test(flavor = "multi_thread")]
10090    async fn test_diameter_single_arc_segment_with_label_position() {
10091        let initial_source = "\
10092sketch(on = XY) {
10093  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10094}
10095";
10096
10097        let program = Program::parse(initial_source).unwrap().0.unwrap();
10098        let mut frontend = FrontendState::new();
10099        let mock_ctx = ExecutorContext::new_mock(None).await;
10100        let version = Version(0);
10101
10102        frontend.program = program.clone();
10103        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10104        frontend.update_state_after_exec(outcome, true);
10105        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10106        let sketch_id = sketch_object.id;
10107        let sketch = expect_sketch(sketch_object);
10108        let arc_id = sketch
10109            .segments
10110            .iter()
10111            .find(|&seg_id| {
10112                let obj = frontend.scene_graph.objects.get(seg_id.0);
10113                matches!(
10114                    obj.map(|o| &o.kind),
10115                    Some(ObjectKind::Segment {
10116                        segment: Segment::Arc(_)
10117                    })
10118                )
10119            })
10120            .unwrap();
10121
10122        let label_position = Point2d {
10123            x: Number {
10124                value: 10.0,
10125                units: NumericSuffix::Mm,
10126            },
10127            y: Number {
10128                value: 11.0,
10129                units: NumericSuffix::Mm,
10130            },
10131        };
10132        let constraint = Constraint::Diameter(Diameter {
10133            arc: *arc_id,
10134            diameter: Number {
10135                value: 10.0,
10136                units: NumericSuffix::Mm,
10137            },
10138            label_position: Some(label_position.clone()),
10139            source: Default::default(),
10140        });
10141        let (src_delta, scene_delta) = frontend
10142            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10143            .await
10144            .unwrap();
10145        assert_eq!(
10146            src_delta.text.as_str(),
10147            "\
10148sketch(on = XY) {
10149  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10150  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10151}
10152"
10153        );
10154
10155        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10156        let sketch = expect_sketch(sketch_object);
10157        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10158        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10159            panic!("Expected constraint object");
10160        };
10161        let Constraint::Diameter(diameter) = constraint else {
10162            panic!("Expected diameter constraint");
10163        };
10164        assert_eq!(diameter.label_position, Some(label_position));
10165
10166        mock_ctx.close().await;
10167    }
10168
10169    #[tokio::test(flavor = "multi_thread")]
10170    async fn test_edit_diameter_constraint_label_position() {
10171        let initial_source = "\
10172sketch(on = XY) {
10173  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10174  diameter(arc1) == 10mm
10175}
10176";
10177
10178        let program = Program::parse(initial_source).unwrap().0.unwrap();
10179        let mut frontend = FrontendState::new();
10180        let mock_ctx = ExecutorContext::new_mock(None).await;
10181        let version = Version(0);
10182
10183        frontend.program = program.clone();
10184        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10185        frontend.update_state_after_exec(outcome, true);
10186        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10187        let sketch_id = sketch_object.id;
10188        let sketch = expect_sketch(sketch_object);
10189        let constraint_id = sketch.constraints[0];
10190        let label_position = Point2d {
10191            x: Number {
10192                value: 10.0,
10193                units: NumericSuffix::Mm,
10194            },
10195            y: Number {
10196                value: 11.0,
10197                units: NumericSuffix::Mm,
10198            },
10199        };
10200
10201        let (src_delta, scene_delta) = frontend
10202            .edit_distance_constraint_label_position(
10203                &mock_ctx,
10204                version,
10205                sketch_id,
10206                constraint_id,
10207                label_position.clone(),
10208                vec![],
10209            )
10210            .await
10211            .unwrap();
10212        assert_eq!(
10213            src_delta.text.as_str(),
10214            "\
10215sketch(on = XY) {
10216  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10217  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10218}
10219"
10220        );
10221
10222        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10223        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10224            panic!("Expected constraint object");
10225        };
10226        let Constraint::Diameter(diameter) = constraint else {
10227            panic!("Expected diameter constraint");
10228        };
10229        assert_eq!(diameter.label_position, Some(label_position));
10230
10231        mock_ctx.close().await;
10232    }
10233
10234    #[tokio::test(flavor = "multi_thread")]
10235    async fn test_diameter_error_cases() {
10236        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237        let mock_ctx = ExecutorContext::new_mock(None).await;
10238        let version = Version(0);
10239
10240        // Test: Single point should error
10241        let initial_source_point = "\
10242sketch(on = XY) {
10243  point(at = [var 1, var 2])
10244}
10245";
10246        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10247        let mut frontend_point = FrontendState::new();
10248        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10249        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10250        let sketch_id_point = sketch_object_point.id;
10251        let sketch_point = expect_sketch(sketch_object_point);
10252        let point_id = *sketch_point.segments.first().unwrap();
10253
10254        let constraint_point = Constraint::Diameter(Diameter {
10255            arc: point_id,
10256            diameter: Number {
10257                value: 10.0,
10258                units: NumericSuffix::Mm,
10259            },
10260            label_position: None,
10261            source: Default::default(),
10262        });
10263        let result_point = frontend_point
10264            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10265            .await;
10266        assert!(result_point.is_err(), "Single point should error for diameter");
10267
10268        // Test: Single line segment should error (only arc segments supported)
10269        let initial_source_line = "\
10270sketch(on = XY) {
10271  line(start = [var 1, var 2], end = [var 3, var 4])
10272}
10273";
10274        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10275        let mut frontend_line = FrontendState::new();
10276        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10277        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10278        let sketch_id_line = sketch_object_line.id;
10279        let sketch_line = expect_sketch(sketch_object_line);
10280        let line_id = *sketch_line.segments.first().unwrap();
10281
10282        let constraint_line = Constraint::Diameter(Diameter {
10283            arc: line_id,
10284            diameter: Number {
10285                value: 10.0,
10286                units: NumericSuffix::Mm,
10287            },
10288            label_position: None,
10289            source: Default::default(),
10290        });
10291        let result_line = frontend_line
10292            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10293            .await;
10294        assert!(result_line.is_err(), "Single line segment should error for diameter");
10295
10296        ctx.close().await;
10297        mock_ctx.close().await;
10298    }
10299
10300    #[tokio::test(flavor = "multi_thread")]
10301    async fn test_line_horizontal() {
10302        let initial_source = "\
10303sketch(on = XY) {
10304  line(start = [var 1, var 2], end = [var 3, var 4])
10305}
10306";
10307
10308        let program = Program::parse(initial_source).unwrap().0.unwrap();
10309
10310        let mut frontend = FrontendState::new();
10311
10312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10313        let mock_ctx = ExecutorContext::new_mock(None).await;
10314        let version = Version(0);
10315
10316        frontend.hack_set_program(&ctx, program).await.unwrap();
10317        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10318        let sketch_id = sketch_object.id;
10319        let sketch = expect_sketch(sketch_object);
10320        let line1_id = *sketch.segments.get(2).unwrap();
10321
10322        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10323        let (src_delta, scene_delta) = frontend
10324            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10325            .await
10326            .unwrap();
10327        assert_eq!(
10328            src_delta.text.as_str(),
10329            "\
10330sketch(on = XY) {
10331  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10332  horizontal(line1)
10333}
10334"
10335        );
10336        assert_eq!(
10337            scene_delta.new_graph.objects.len(),
10338            6,
10339            "{:#?}",
10340            scene_delta.new_graph.objects
10341        );
10342
10343        ctx.close().await;
10344        mock_ctx.close().await;
10345    }
10346
10347    #[tokio::test(flavor = "multi_thread")]
10348    async fn test_line_vertical() {
10349        let initial_source = "\
10350sketch(on = XY) {
10351  line(start = [var 1, var 2], end = [var 3, var 4])
10352}
10353";
10354
10355        let program = Program::parse(initial_source).unwrap().0.unwrap();
10356
10357        let mut frontend = FrontendState::new();
10358
10359        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10360        let mock_ctx = ExecutorContext::new_mock(None).await;
10361        let version = Version(0);
10362
10363        frontend.hack_set_program(&ctx, program).await.unwrap();
10364        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10365        let sketch_id = sketch_object.id;
10366        let sketch = expect_sketch(sketch_object);
10367        let line1_id = *sketch.segments.get(2).unwrap();
10368
10369        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10370        let (src_delta, scene_delta) = frontend
10371            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10372            .await
10373            .unwrap();
10374        assert_eq!(
10375            src_delta.text.as_str(),
10376            "\
10377sketch(on = XY) {
10378  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10379  vertical(line1)
10380}
10381"
10382        );
10383        assert_eq!(
10384            scene_delta.new_graph.objects.len(),
10385            6,
10386            "{:#?}",
10387            scene_delta.new_graph.objects
10388        );
10389
10390        ctx.close().await;
10391        mock_ctx.close().await;
10392    }
10393
10394    #[tokio::test(flavor = "multi_thread")]
10395    async fn test_points_vertical() {
10396        let initial_source = "\
10397sketch001 = sketch(on = XY) {
10398  p0 = point(at = [var -2.23mm, var 3.1mm])
10399  pf = point(at = [4, 4])
10400}
10401";
10402
10403        let program = Program::parse(initial_source).unwrap().0.unwrap();
10404
10405        let mut frontend = FrontendState::new();
10406
10407        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10408        let mock_ctx = ExecutorContext::new_mock(None).await;
10409        let version = Version(0);
10410
10411        frontend.hack_set_program(&ctx, program).await.unwrap();
10412        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10413        let sketch_id = sketch_object.id;
10414        let sketch = expect_sketch(sketch_object);
10415        let point_ids = vec![
10416            sketch.segments.first().unwrap().to_owned(),
10417            sketch.segments.get(1).unwrap().to_owned(),
10418        ];
10419
10420        let constraint = Constraint::Vertical(Vertical::Points {
10421            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10422        });
10423        let (src_delta, scene_delta) = frontend
10424            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10425            .await
10426            .unwrap();
10427        assert_eq!(
10428            src_delta.text.as_str(),
10429            "\
10430sketch001 = sketch(on = XY) {
10431  p0 = point(at = [var -2.23mm, var 3.1mm])
10432  pf = point(at = [4, 4])
10433  vertical([p0, pf])
10434}
10435"
10436        );
10437        assert_eq!(
10438            scene_delta.new_graph.objects.len(),
10439            5,
10440            "{:#?}",
10441            scene_delta.new_graph.objects
10442        );
10443
10444        ctx.close().await;
10445        mock_ctx.close().await;
10446    }
10447
10448    #[tokio::test(flavor = "multi_thread")]
10449    async fn test_points_horizontal() {
10450        let initial_source = "\
10451sketch001 = sketch(on = XY) {
10452  p0 = point(at = [var -2.23mm, var 3.1mm])
10453  pf = point(at = [4, 4])
10454}
10455";
10456
10457        let program = Program::parse(initial_source).unwrap().0.unwrap();
10458
10459        let mut frontend = FrontendState::new();
10460
10461        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10462        let mock_ctx = ExecutorContext::new_mock(None).await;
10463        let version = Version(0);
10464
10465        frontend.hack_set_program(&ctx, program).await.unwrap();
10466        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10467        let sketch_id = sketch_object.id;
10468        let sketch = expect_sketch(sketch_object);
10469        let point_ids = vec![
10470            sketch.segments.first().unwrap().to_owned(),
10471            sketch.segments.get(1).unwrap().to_owned(),
10472        ];
10473
10474        let constraint = Constraint::Horizontal(Horizontal::Points {
10475            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10476        });
10477        let (src_delta, scene_delta) = frontend
10478            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10479            .await
10480            .unwrap();
10481        assert_eq!(
10482            src_delta.text.as_str(),
10483            "\
10484sketch001 = sketch(on = XY) {
10485  p0 = point(at = [var -2.23mm, var 3.1mm])
10486  pf = point(at = [4, 4])
10487  horizontal([p0, pf])
10488}
10489"
10490        );
10491        assert_eq!(
10492            scene_delta.new_graph.objects.len(),
10493            5,
10494            "{:#?}",
10495            scene_delta.new_graph.objects
10496        );
10497
10498        ctx.close().await;
10499        mock_ctx.close().await;
10500    }
10501
10502    #[tokio::test(flavor = "multi_thread")]
10503    async fn test_point_horizontal_with_origin() {
10504        let initial_source = "\
10505sketch001 = sketch(on = XY) {
10506  p0 = point(at = [var -2.23mm, var 3.1mm])
10507}
10508";
10509
10510        let program = Program::parse(initial_source).unwrap().0.unwrap();
10511
10512        let mut frontend = FrontendState::new();
10513
10514        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10515        let mock_ctx = ExecutorContext::new_mock(None).await;
10516        let version = Version(0);
10517
10518        frontend.hack_set_program(&ctx, program).await.unwrap();
10519        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10520        let sketch_id = sketch_object.id;
10521        let sketch = expect_sketch(sketch_object);
10522        let point_id = *sketch.segments.first().unwrap();
10523
10524        let constraint = Constraint::Horizontal(Horizontal::Points {
10525            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10526        });
10527        let (src_delta, scene_delta) = frontend
10528            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10529            .await
10530            .unwrap();
10531        assert_eq!(
10532            src_delta.text.as_str(),
10533            "\
10534sketch001 = sketch(on = XY) {
10535  p0 = point(at = [var -2.23mm, var 3.1mm])
10536  horizontal([p0, ORIGIN])
10537}
10538"
10539        );
10540        assert_eq!(
10541            scene_delta.new_graph.objects.len(),
10542            4,
10543            "{:#?}",
10544            scene_delta.new_graph.objects
10545        );
10546
10547        ctx.close().await;
10548        mock_ctx.close().await;
10549    }
10550
10551    #[tokio::test(flavor = "multi_thread")]
10552    async fn test_lines_equal_length() {
10553        let initial_source = "\
10554sketch(on = XY) {
10555  line(start = [var 1, var 2], end = [var 3, var 4])
10556  line(start = [var 5, var 6], end = [var 7, var 8])
10557}
10558";
10559
10560        let program = Program::parse(initial_source).unwrap().0.unwrap();
10561
10562        let mut frontend = FrontendState::new();
10563
10564        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10565        let mock_ctx = ExecutorContext::new_mock(None).await;
10566        let version = Version(0);
10567
10568        frontend.hack_set_program(&ctx, program).await.unwrap();
10569        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10570        let sketch_id = sketch_object.id;
10571        let sketch = expect_sketch(sketch_object);
10572        let line1_id = *sketch.segments.get(2).unwrap();
10573        let line2_id = *sketch.segments.get(5).unwrap();
10574
10575        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10576            lines: vec![line1_id, line2_id],
10577        });
10578        let (src_delta, scene_delta) = frontend
10579            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10580            .await
10581            .unwrap();
10582        assert_eq!(
10583            src_delta.text.as_str(),
10584            "\
10585sketch(on = XY) {
10586  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10587  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10588  equalLength([line1, line2])
10589}
10590"
10591        );
10592        assert_eq!(
10593            scene_delta.new_graph.objects.len(),
10594            9,
10595            "{:#?}",
10596            scene_delta.new_graph.objects
10597        );
10598
10599        ctx.close().await;
10600        mock_ctx.close().await;
10601    }
10602
10603    #[tokio::test(flavor = "multi_thread")]
10604    async fn test_add_constraint_multi_line_equal_length() {
10605        let initial_source = "\
10606sketch(on = XY) {
10607  line(start = [var 1, var 2], end = [var 3, var 4])
10608  line(start = [var 5, var 6], end = [var 7, var 8])
10609  line(start = [var 9, var 10], end = [var 11, var 12])
10610}
10611";
10612
10613        let program = Program::parse(initial_source).unwrap().0.unwrap();
10614
10615        let mut frontend = FrontendState::new();
10616        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10617        let mock_ctx = ExecutorContext::new_mock(None).await;
10618        let version = Version(0);
10619
10620        frontend.hack_set_program(&ctx, program).await.unwrap();
10621        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10622        let sketch_id = sketch_object.id;
10623        let sketch = expect_sketch(sketch_object);
10624        let line1_id = *sketch.segments.get(2).unwrap();
10625        let line2_id = *sketch.segments.get(5).unwrap();
10626        let line3_id = *sketch.segments.get(8).unwrap();
10627
10628        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10629            lines: vec![line1_id, line2_id, line3_id],
10630        });
10631        let (src_delta, scene_delta) = frontend
10632            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10633            .await
10634            .unwrap();
10635        assert_eq!(
10636            src_delta.text.as_str(),
10637            "\
10638sketch(on = XY) {
10639  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10640  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10641  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10642  equalLength([line1, line2, line3])
10643}
10644"
10645        );
10646        let constraints = scene_delta
10647            .new_graph
10648            .objects
10649            .iter()
10650            .filter_map(|obj| {
10651                let ObjectKind::Constraint { constraint } = &obj.kind else {
10652                    return None;
10653                };
10654                Some(constraint)
10655            })
10656            .collect::<Vec<_>>();
10657
10658        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10659        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10660            panic!("expected equal length constraint, got {:?}", constraints[0]);
10661        };
10662        assert_eq!(lines_equal_length.lines.len(), 3);
10663
10664        ctx.close().await;
10665        mock_ctx.close().await;
10666    }
10667
10668    #[tokio::test(flavor = "multi_thread")]
10669    async fn test_lines_parallel() {
10670        let initial_source = "\
10671sketch(on = XY) {
10672  line(start = [var 1, var 2], end = [var 3, var 4])
10673  line(start = [var 5, var 6], end = [var 7, var 8])
10674}
10675";
10676
10677        let program = Program::parse(initial_source).unwrap().0.unwrap();
10678
10679        let mut frontend = FrontendState::new();
10680
10681        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10682        let mock_ctx = ExecutorContext::new_mock(None).await;
10683        let version = Version(0);
10684
10685        frontend.hack_set_program(&ctx, program).await.unwrap();
10686        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10687        let sketch_id = sketch_object.id;
10688        let sketch = expect_sketch(sketch_object);
10689        let line1_id = *sketch.segments.get(2).unwrap();
10690        let line2_id = *sketch.segments.get(5).unwrap();
10691
10692        let constraint = Constraint::Parallel(Parallel {
10693            lines: vec![line1_id, line2_id],
10694        });
10695        let (src_delta, scene_delta) = frontend
10696            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10697            .await
10698            .unwrap();
10699        assert_eq!(
10700            src_delta.text.as_str(),
10701            "\
10702sketch(on = XY) {
10703  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10704  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10705  parallel([line1, line2])
10706}
10707"
10708        );
10709        assert_eq!(
10710            scene_delta.new_graph.objects.len(),
10711            9,
10712            "{:#?}",
10713            scene_delta.new_graph.objects
10714        );
10715
10716        ctx.close().await;
10717        mock_ctx.close().await;
10718    }
10719
10720    #[tokio::test(flavor = "multi_thread")]
10721    async fn test_lines_parallel_multiline() {
10722        let initial_source = "\
10723sketch(on = XY) {
10724  line(start = [var 1, var 2], end = [var 3, var 4])
10725  line(start = [var 5, var 6], end = [var 7, var 8])
10726  line(start = [var 9, var 10], end = [var 11, var 12])
10727}
10728";
10729
10730        let program = Program::parse(initial_source).unwrap().0.unwrap();
10731
10732        let mut frontend = FrontendState::new();
10733
10734        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10735        let mock_ctx = ExecutorContext::new_mock(None).await;
10736        let version = Version(0);
10737
10738        frontend.hack_set_program(&ctx, program).await.unwrap();
10739        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10740        let sketch_id = sketch_object.id;
10741        let sketch = expect_sketch(sketch_object);
10742        let line1_id = *sketch.segments.get(2).unwrap();
10743        let line2_id = *sketch.segments.get(5).unwrap();
10744        let line3_id = *sketch.segments.get(8).unwrap();
10745
10746        let constraint = Constraint::Parallel(Parallel {
10747            lines: vec![line1_id, line2_id, line3_id],
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  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10758  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10759  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10760  parallel([line1, line2, line3])
10761}
10762"
10763        );
10764
10765        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10766        let sketch = expect_sketch(sketch_object);
10767        assert_eq!(sketch.constraints.len(), 1);
10768
10769        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10770        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10771            panic!("Expected constraint object");
10772        };
10773        let Constraint::Parallel(parallel) = constraint else {
10774            panic!("Expected parallel constraint");
10775        };
10776        assert_eq!(parallel.lines.len(), 3);
10777
10778        ctx.close().await;
10779        mock_ctx.close().await;
10780    }
10781
10782    #[tokio::test(flavor = "multi_thread")]
10783    async fn test_lines_perpendicular() {
10784        let initial_source = "\
10785sketch(on = XY) {
10786  line(start = [var 1, var 2], end = [var 3, var 4])
10787  line(start = [var 5, var 6], end = [var 7, var 8])
10788}
10789";
10790
10791        let program = Program::parse(initial_source).unwrap().0.unwrap();
10792
10793        let mut frontend = FrontendState::new();
10794
10795        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10796        let mock_ctx = ExecutorContext::new_mock(None).await;
10797        let version = Version(0);
10798
10799        frontend.hack_set_program(&ctx, program).await.unwrap();
10800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10801        let sketch_id = sketch_object.id;
10802        let sketch = expect_sketch(sketch_object);
10803        let line1_id = *sketch.segments.get(2).unwrap();
10804        let line2_id = *sketch.segments.get(5).unwrap();
10805
10806        let constraint = Constraint::Perpendicular(Perpendicular {
10807            lines: vec![line1_id, line2_id],
10808        });
10809        let (src_delta, scene_delta) = frontend
10810            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10811            .await
10812            .unwrap();
10813        assert_eq!(
10814            src_delta.text.as_str(),
10815            "\
10816sketch(on = XY) {
10817  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10818  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10819  perpendicular([line1, line2])
10820}
10821"
10822        );
10823        assert_eq!(
10824            scene_delta.new_graph.objects.len(),
10825            9,
10826            "{:#?}",
10827            scene_delta.new_graph.objects
10828        );
10829
10830        ctx.close().await;
10831        mock_ctx.close().await;
10832    }
10833
10834    #[tokio::test(flavor = "multi_thread")]
10835    async fn test_lines_angle() {
10836        let initial_source = "\
10837sketch(on = XY) {
10838  line(start = [var 1, var 2], end = [var 3, var 4])
10839  line(start = [var 5, var 6], end = [var 7, var 8])
10840}
10841";
10842
10843        let program = Program::parse(initial_source).unwrap().0.unwrap();
10844
10845        let mut frontend = FrontendState::new();
10846
10847        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10848        let mock_ctx = ExecutorContext::new_mock(None).await;
10849        let version = Version(0);
10850
10851        frontend.hack_set_program(&ctx, program).await.unwrap();
10852        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10853        let sketch_id = sketch_object.id;
10854        let sketch = expect_sketch(sketch_object);
10855        let line1_id = *sketch.segments.get(2).unwrap();
10856        let line2_id = *sketch.segments.get(5).unwrap();
10857
10858        let constraint = Constraint::Angle(Angle {
10859            lines: vec![line1_id, line2_id],
10860            angle: Number {
10861                value: 30.0,
10862                units: NumericSuffix::Deg,
10863            },
10864            source: Default::default(),
10865        });
10866        let (src_delta, scene_delta) = frontend
10867            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10868            .await
10869            .unwrap();
10870        assert_eq!(
10871            src_delta.text.as_str(),
10872            // The lack indentation is a formatter bug.
10873            "\
10874sketch(on = XY) {
10875  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10876  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10877  angle([line1, line2]) == 30deg
10878}
10879"
10880        );
10881        assert_eq!(
10882            scene_delta.new_graph.objects.len(),
10883            9,
10884            "{:#?}",
10885            scene_delta.new_graph.objects
10886        );
10887
10888        ctx.close().await;
10889        mock_ctx.close().await;
10890    }
10891
10892    #[tokio::test(flavor = "multi_thread")]
10893    async fn test_segments_tangent() {
10894        let initial_source = "\
10895sketch(on = XY) {
10896  line(start = [var 1, var 2], end = [var 3, var 4])
10897  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10898}
10899";
10900
10901        let program = Program::parse(initial_source).unwrap().0.unwrap();
10902
10903        let mut frontend = FrontendState::new();
10904
10905        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10906        let mock_ctx = ExecutorContext::new_mock(None).await;
10907        let version = Version(0);
10908
10909        frontend.hack_set_program(&ctx, program).await.unwrap();
10910        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10911        let sketch_id = sketch_object.id;
10912        let sketch = expect_sketch(sketch_object);
10913        let line1_id = *sketch.segments.get(2).unwrap();
10914        let arc1_id = *sketch.segments.get(6).unwrap();
10915
10916        let constraint = Constraint::Tangent(Tangent {
10917            input: vec![line1_id, arc1_id],
10918        });
10919        let (src_delta, scene_delta) = frontend
10920            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10921            .await
10922            .unwrap();
10923        assert_eq!(
10924            src_delta.text.as_str(),
10925            "\
10926sketch(on = XY) {
10927  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10928  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10929  tangent([line1, arc1])
10930}
10931"
10932        );
10933        assert_eq!(
10934            scene_delta.new_graph.objects.len(),
10935            10,
10936            "{:#?}",
10937            scene_delta.new_graph.objects
10938        );
10939
10940        ctx.close().await;
10941        mock_ctx.close().await;
10942    }
10943
10944    #[tokio::test(flavor = "multi_thread")]
10945    async fn test_point_midpoint() {
10946        let initial_source = "\
10947sketch(on = XY) {
10948  point(at = [var 1, var 1])
10949  line(start = [var 0, var 0], end = [var 6, var 4])
10950}
10951";
10952
10953        let program = Program::parse(initial_source).unwrap().0.unwrap();
10954
10955        let mut frontend = FrontendState::new();
10956
10957        let ctx = ExecutorContext::new_mock(None).await;
10958        let version = Version(0);
10959
10960        frontend.program = program.clone();
10961        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10962        frontend.update_state_after_exec(outcome, true);
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.segments.get(3).unwrap();
10968
10969        let constraint = Constraint::Midpoint(Midpoint {
10970            point: point_id,
10971            segment: line_id,
10972        });
10973        let (src_delta, scene_delta) = frontend
10974            .add_constraint(&ctx, version, sketch_id, constraint)
10975            .await
10976            .unwrap();
10977        assert_eq!(
10978            src_delta.text.as_str(),
10979            "\
10980sketch(on = XY) {
10981  point1 = point(at = [var 1, var 1])
10982  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10983  midpoint(line1, point = point1)
10984}
10985"
10986        );
10987        assert_eq!(
10988            scene_delta.new_graph.objects.len(),
10989            7,
10990            "{:#?}",
10991            scene_delta.new_graph.objects
10992        );
10993
10994        ctx.close().await;
10995    }
10996
10997    #[tokio::test(flavor = "multi_thread")]
10998    async fn test_segments_symmetric() {
10999        let initial_source = "\
11000sketch(on = XY) {
11001  line(start = [var 0, var 0], end = [var 0, var 4])
11002  line(start = [var 4, var 0], end = [var 4, var 4])
11003  line(start = [var 2, var -1], end = [var 2, var 5])
11004}
11005";
11006
11007        let program = Program::parse(initial_source).unwrap().0.unwrap();
11008
11009        let mut frontend = FrontendState::new();
11010
11011        let ctx = ExecutorContext::new_mock(None).await;
11012        let version = Version(0);
11013
11014        frontend.program = program.clone();
11015        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11016        frontend.update_state_after_exec(outcome, true);
11017        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11018        let sketch_id = sketch_object.id;
11019        let sketch = expect_sketch(sketch_object);
11020        let line1_id = *sketch.segments.get(2).unwrap();
11021        let line2_id = *sketch.segments.get(5).unwrap();
11022        let axis_id = *sketch.segments.get(8).unwrap();
11023
11024        let constraint = Constraint::Symmetric(Symmetric {
11025            input: vec![line1_id, line2_id],
11026            axis: axis_id,
11027        });
11028        let (src_delta, scene_delta) = frontend
11029            .add_constraint(&ctx, version, sketch_id, constraint)
11030            .await
11031            .unwrap();
11032        assert_eq!(
11033            src_delta.text.as_str(),
11034            "\
11035sketch(on = XY) {
11036  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11037  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11038  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11039  symmetric([line1, line2], axis = line3)
11040}
11041"
11042        );
11043        assert_eq!(
11044            scene_delta.new_graph.objects.len(),
11045            12,
11046            "{:#?}",
11047            scene_delta.new_graph.objects
11048        );
11049
11050        ctx.close().await;
11051    }
11052
11053    #[tokio::test(flavor = "multi_thread")]
11054    async fn test_point_arc_midpoint() {
11055        let initial_source = "\
11056sketch(on = XY) {
11057  point(at = [var 6, var 3])
11058  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11059}
11060";
11061
11062        let program = Program::parse(initial_source).unwrap().0.unwrap();
11063
11064        let mut frontend = FrontendState::new();
11065
11066        let ctx = ExecutorContext::new_mock(None).await;
11067        let version = Version(0);
11068
11069        frontend.program = program.clone();
11070        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11071        frontend.update_state_after_exec(outcome, true);
11072        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11073        let sketch_id = sketch_object.id;
11074        let sketch = expect_sketch(sketch_object);
11075        let point_id = *sketch.segments.first().unwrap();
11076        let arc_id = *sketch.segments.get(4).unwrap();
11077
11078        let constraint = Constraint::Midpoint(Midpoint {
11079            point: point_id,
11080            segment: arc_id,
11081        });
11082        let (src_delta, scene_delta) = frontend
11083            .add_constraint(&ctx, version, sketch_id, constraint)
11084            .await
11085            .unwrap();
11086        assert_eq!(
11087            src_delta.text.as_str(),
11088            "\
11089sketch(on = XY) {
11090  point1 = point(at = [var 6, var 3])
11091  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11092  midpoint(arc1, point = point1)
11093}
11094"
11095        );
11096        assert_eq!(
11097            scene_delta.new_graph.objects.len(),
11098            8,
11099            "{:#?}",
11100            scene_delta.new_graph.objects
11101        );
11102
11103        ctx.close().await;
11104    }
11105
11106    #[tokio::test(flavor = "multi_thread")]
11107    async fn test_segments_symmetric_arcs() {
11108        let initial_source = "\
11109sketch(on = XY) {
11110  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11111  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11112  line(start = [var 0, var -10], end = [var 0, var 10])
11113}
11114";
11115
11116        let program = Program::parse(initial_source).unwrap().0.unwrap();
11117
11118        let mut frontend = FrontendState::new();
11119
11120        let ctx = ExecutorContext::new_mock(None).await;
11121        let version = Version(0);
11122
11123        frontend.program = program.clone();
11124        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11125        frontend.update_state_after_exec(outcome, true);
11126        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11127        let sketch_id = sketch_object.id;
11128        let sketch = expect_sketch(sketch_object);
11129        let arc1_id = *sketch.segments.get(3).unwrap();
11130        let arc2_id = *sketch.segments.get(7).unwrap();
11131        let axis_id = *sketch.segments.get(10).unwrap();
11132
11133        let constraint = Constraint::Symmetric(Symmetric {
11134            input: vec![arc1_id, arc2_id],
11135            axis: axis_id,
11136        });
11137        let (src_delta, scene_delta) = frontend
11138            .add_constraint(&ctx, version, sketch_id, constraint)
11139            .await
11140            .unwrap();
11141        assert_eq!(
11142            src_delta.text.as_str(),
11143            "\
11144sketch(on = XY) {
11145  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11146  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11147  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11148  symmetric([arc1, arc2], axis = line1)
11149}
11150"
11151        );
11152        assert_eq!(
11153            scene_delta.new_graph.objects.len(),
11154            14,
11155            "{:#?}",
11156            scene_delta.new_graph.objects
11157        );
11158
11159        ctx.close().await;
11160    }
11161
11162    #[tokio::test(flavor = "multi_thread")]
11163    async fn test_sketch_on_face_simple() {
11164        let initial_source = "\
11165len = 2mm
11166cube = startSketchOn(XY)
11167  |> startProfile(at = [0, 0])
11168  |> line(end = [len, 0], tag = $side)
11169  |> line(end = [0, len])
11170  |> line(end = [-len, 0])
11171  |> line(end = [0, -len])
11172  |> close()
11173  |> extrude(length = len)
11174
11175face = faceOf(cube, face = side)
11176";
11177
11178        let program = Program::parse(initial_source).unwrap().0.unwrap();
11179
11180        let mut frontend = FrontendState::new();
11181
11182        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11183        let mock_ctx = ExecutorContext::new_mock(None).await;
11184        let version = Version(0);
11185
11186        frontend.hack_set_program(&ctx, program).await.unwrap();
11187        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11188        let face_id = face_object.id;
11189
11190        let sketch_args = SketchCtor {
11191            on: Plane::Object(face_id),
11192        };
11193        let (_src_delta, scene_delta, sketch_id) = frontend
11194            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11195            .await
11196            .unwrap();
11197        assert_eq!(sketch_id, ObjectId(2));
11198        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11199        let sketch_object = &scene_delta.new_graph.objects[2];
11200        assert_eq!(sketch_object.id, ObjectId(2));
11201        assert_eq!(
11202            sketch_object.kind,
11203            ObjectKind::Sketch(Sketch {
11204                args: SketchCtor {
11205                    on: Plane::Object(face_id),
11206                },
11207                plane: face_id,
11208                segments: vec![],
11209                constraints: vec![],
11210            })
11211        );
11212        assert_eq!(scene_delta.new_graph.objects.len(), 8);
11213
11214        ctx.close().await;
11215        mock_ctx.close().await;
11216    }
11217
11218    #[tokio::test(flavor = "multi_thread")]
11219    async fn test_sketch_on_wall_artifact_from_region_extrude() {
11220        let initial_source = "\
11221s = sketch(on = YZ) {
11222  line1 = line(start = [0, 0], end = [0, 1])
11223  line2 = line(start = [0, 1], end = [1, 1])
11224  line3 = line(start = [1, 1], end = [0, 0])
11225}
11226region001 = region(point = [0.1, 0.1], sketch = s)
11227extrude001 = extrude(region001, length = 5)
11228";
11229
11230        let program = Program::parse(initial_source).unwrap().0.unwrap();
11231
11232        let mut frontend = FrontendState::new();
11233        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11234        let version = Version(0);
11235
11236        frontend.hack_set_program(&ctx, program).await.unwrap();
11237        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11238
11239        let sketch_args = SketchCtor {
11240            on: Plane::Object(wall_object_id),
11241        };
11242        let (src_delta, _scene_delta, _sketch_id) = frontend
11243            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11244            .await
11245            .unwrap();
11246        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11247
11248        ctx.close().await;
11249    }
11250
11251    #[tokio::test(flavor = "multi_thread")]
11252    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11253        let initial_source = "\
11254sketch001 = sketch(on = YZ) {
11255  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11256  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11257  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11258  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11259  coincident([line1.end, line2.start])
11260  coincident([line2.end, line3.start])
11261  coincident([line3.end, line4.start])
11262  coincident([line4.end, line1.start])
11263  parallel([line2, line4])
11264  parallel([line3, line1])
11265  perpendicular([line1, line2])
11266  horizontal(line3)
11267  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11268}
11269region001 = region(point = [3.1, 3.74], sketch = sketch001)
11270extrude001 = extrude(region001, length = 5)
11271";
11272
11273        let program = Program::parse(initial_source).unwrap().0.unwrap();
11274
11275        let mut frontend = FrontendState::new();
11276        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11277        let version = Version(0);
11278
11279        frontend.hack_set_program(&ctx, program).await.unwrap();
11280        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11281
11282        let sketch_args = SketchCtor {
11283            on: Plane::Object(wall_object_id),
11284        };
11285        let (src_delta, _scene_delta, _sketch_id) = frontend
11286            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11287            .await
11288            .unwrap();
11289        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11290
11291        ctx.close().await;
11292    }
11293
11294    #[tokio::test(flavor = "multi_thread")]
11295    async fn test_sketch_on_plane_incremental() {
11296        let initial_source = "\
11297len = 2mm
11298cube = startSketchOn(XY)
11299  |> startProfile(at = [0, 0])
11300  |> line(end = [len, 0], tag = $side)
11301  |> line(end = [0, len])
11302  |> line(end = [-len, 0])
11303  |> line(end = [0, -len])
11304  |> close()
11305  |> extrude(length = len)
11306
11307plane = planeOf(cube, face = side)
11308";
11309
11310        let program = Program::parse(initial_source).unwrap().0.unwrap();
11311
11312        let mut frontend = FrontendState::new();
11313
11314        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11315        let mock_ctx = ExecutorContext::new_mock(None).await;
11316        let version = Version(0);
11317
11318        frontend.hack_set_program(&ctx, program).await.unwrap();
11319        // Find the last plane since the first plane is the XY plane.
11320        let plane_object = frontend
11321            .scene_graph
11322            .objects
11323            .iter()
11324            .rev()
11325            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11326            .unwrap();
11327        let plane_id = plane_object.id;
11328
11329        let sketch_args = SketchCtor {
11330            on: Plane::Object(plane_id),
11331        };
11332        let (src_delta, scene_delta, sketch_id) = frontend
11333            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11334            .await
11335            .unwrap();
11336        assert_eq!(
11337            src_delta.text.as_str(),
11338            "\
11339len = 2mm
11340cube = startSketchOn(XY)
11341  |> startProfile(at = [0, 0])
11342  |> line(end = [len, 0], tag = $side)
11343  |> line(end = [0, len])
11344  |> line(end = [-len, 0])
11345  |> line(end = [0, -len])
11346  |> close()
11347  |> extrude(length = len)
11348
11349plane = planeOf(cube, face = side)
11350sketch001 = sketch(on = plane) {
11351}
11352"
11353        );
11354        assert_eq!(sketch_id, ObjectId(2));
11355        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11356        let sketch_object = &scene_delta.new_graph.objects[2];
11357        assert_eq!(sketch_object.id, ObjectId(2));
11358        assert_eq!(
11359            sketch_object.kind,
11360            ObjectKind::Sketch(Sketch {
11361                args: SketchCtor {
11362                    on: Plane::Object(plane_id),
11363                },
11364                plane: plane_id,
11365                segments: vec![],
11366                constraints: vec![],
11367            })
11368        );
11369        assert_eq!(scene_delta.new_graph.objects.len(), 9);
11370
11371        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11372        assert_eq!(plane_object.id, plane_id);
11373        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11374
11375        ctx.close().await;
11376        mock_ctx.close().await;
11377    }
11378
11379    #[tokio::test(flavor = "multi_thread")]
11380    async fn test_new_sketch_uses_unique_variable_name() {
11381        let initial_source = "\
11382sketch1 = sketch(on = XY) {
11383}
11384";
11385
11386        let program = Program::parse(initial_source).unwrap().0.unwrap();
11387
11388        let mut frontend = FrontendState::new();
11389        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11390        let version = Version(0);
11391
11392        frontend.hack_set_program(&ctx, program).await.unwrap();
11393
11394        let sketch_args = SketchCtor {
11395            on: Plane::Default(PlaneName::Yz),
11396        };
11397        let (src_delta, _, _) = frontend
11398            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11399            .await
11400            .unwrap();
11401
11402        assert_eq!(
11403            src_delta.text.as_str(),
11404            "\
11405sketch1 = sketch(on = XY) {
11406}
11407sketch001 = sketch(on = YZ) {
11408}
11409"
11410        );
11411
11412        ctx.close().await;
11413    }
11414
11415    #[tokio::test(flavor = "multi_thread")]
11416    async fn test_new_sketch_twice_using_same_plane() {
11417        let initial_source = "\
11418sketch1 = sketch(on = XY) {
11419}
11420";
11421
11422        let program = Program::parse(initial_source).unwrap().0.unwrap();
11423
11424        let mut frontend = FrontendState::new();
11425        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11426        let version = Version(0);
11427
11428        frontend.hack_set_program(&ctx, program).await.unwrap();
11429
11430        let sketch_args = SketchCtor {
11431            on: Plane::Default(PlaneName::Xy),
11432        };
11433        let (src_delta, _, _) = frontend
11434            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11435            .await
11436            .unwrap();
11437
11438        assert_eq!(
11439            src_delta.text.as_str(),
11440            "\
11441sketch1 = sketch(on = XY) {
11442}
11443sketch001 = sketch(on = XY) {
11444}
11445"
11446        );
11447
11448        ctx.close().await;
11449    }
11450
11451    #[tokio::test(flavor = "multi_thread")]
11452    async fn test_sketch_mode_reuses_cached_on_expression() {
11453        let initial_source = "\
11454width = 2mm
11455sketch(on = offsetPlane(XY, offset = width)) {
11456  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11457  distance([line1.start, line1.end]) == width
11458}
11459";
11460        let program = Program::parse(initial_source).unwrap().0.unwrap();
11461
11462        let mut frontend = FrontendState::new();
11463        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11464        let mock_ctx = ExecutorContext::new_mock(None).await;
11465        let version = Version(0);
11466        let project_id = ProjectId(0);
11467        let file_id = FileId(0);
11468
11469        frontend.hack_set_program(&ctx, program).await.unwrap();
11470        let initial_object_count = frontend.scene_graph.objects.len();
11471        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11472            .expect("Expected sketch object to exist")
11473            .id;
11474
11475        // Entering sketch mode should reuse cached `on` expression state
11476        // (offsetPlane result), not fail or create extra on-surface objects.
11477        let scene_delta = frontend
11478            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11479            .await
11480            .unwrap();
11481        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11482
11483        // A follow-up sketch-mode execution should keep the same stable object
11484        // graph shape as well.
11485        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11486        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11487
11488        ctx.close().await;
11489        mock_ctx.close().await;
11490    }
11491
11492    #[tokio::test(flavor = "multi_thread")]
11493    async fn test_multiple_sketch_blocks() {
11494        let initial_source = "\
11495// Cube that requires the engine.
11496width = 2
11497sketch001 = startSketchOn(XY)
11498profile001 = startProfile(sketch001, at = [0, 0])
11499  |> yLine(length = width, tag = $seg1)
11500  |> xLine(length = width)
11501  |> yLine(length = -width)
11502  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11503  |> close()
11504extrude001 = extrude(profile001, length = width)
11505
11506// Get a value that requires the engine.
11507x = segLen(seg1)
11508
11509// Triangle with side length 2*x.
11510sketch(on = XY) {
11511  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11512  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11513  coincident([line1.end, line2.start])
11514  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11515  coincident([line2.end, line3.start])
11516  coincident([line3.end, line1.start])
11517  equalLength([line3, line1])
11518  equalLength([line1, line2])
11519  distance([line1.start, line1.end]) == 2*x
11520}
11521
11522// Line segment with length x.
11523sketch2 = sketch(on = XY) {
11524  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11525  distance([line1.start, line1.end]) == x
11526}
11527";
11528
11529        let program = Program::parse(initial_source).unwrap().0.unwrap();
11530
11531        let mut frontend = FrontendState::new();
11532
11533        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11534        let mock_ctx = ExecutorContext::new_mock(None).await;
11535        let version = Version(0);
11536        let project_id = ProjectId(0);
11537        let file_id = FileId(0);
11538
11539        frontend.hack_set_program(&ctx, program).await.unwrap();
11540        let sketch_objects = frontend
11541            .scene_graph
11542            .objects
11543            .iter()
11544            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11545            .collect::<Vec<_>>();
11546        let sketch1_id = sketch_objects.first().unwrap().id;
11547        let sketch2_id = sketch_objects.get(1).unwrap().id;
11548        // First point in sketch1.
11549        let point1_id = ObjectId(sketch1_id.0 + 1);
11550        // First point in sketch2.
11551        let point2_id = ObjectId(sketch2_id.0 + 1);
11552
11553        // Edit the first sketch. Objects before the sketch block should be
11554        // present from execution cache so that we can sketch on prior planes,
11555        // for example. Objects after the first sketch block should not be
11556        // present since those statements are skipped in sketch mode.
11557        //
11558        // - startSketchOn(XY) Plane 1
11559        // - sketch on=XY Plane 1
11560        // - Sketch block 16
11561        let scene_delta = frontend
11562            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11563            .await
11564            .unwrap();
11565        assert_eq!(
11566            scene_delta.new_graph.objects.len(),
11567            18,
11568            "{:#?}",
11569            scene_delta.new_graph.objects
11570        );
11571
11572        // Edit a point in the first sketch.
11573        let point_ctor = PointCtor {
11574            position: Point2d {
11575                x: Expr::Var(Number {
11576                    value: 1.0,
11577                    units: NumericSuffix::Mm,
11578                }),
11579                y: Expr::Var(Number {
11580                    value: 2.0,
11581                    units: NumericSuffix::Mm,
11582                }),
11583            },
11584        };
11585        let segments = vec![ExistingSegmentCtor {
11586            id: point1_id,
11587            ctor: SegmentCtor::Point(point_ctor),
11588        }];
11589        let (src_delta, _) = frontend
11590            .edit_segments(&mock_ctx, version, sketch1_id, segments)
11591            .await
11592            .unwrap();
11593        // Only the first sketch block changes.
11594        assert_eq!(
11595            src_delta.text.as_str(),
11596            "\
11597// Cube that requires the engine.
11598width = 2
11599sketch001 = startSketchOn(XY)
11600profile001 = startProfile(sketch001, at = [0, 0])
11601  |> yLine(length = width, tag = $seg1)
11602  |> xLine(length = width)
11603  |> yLine(length = -width)
11604  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11605  |> close()
11606extrude001 = extrude(profile001, length = width)
11607
11608// Get a value that requires the engine.
11609x = segLen(seg1)
11610
11611// Triangle with side length 2*x.
11612sketch(on = XY) {
11613  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11614  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11615  coincident([line1.end, line2.start])
11616  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11617  coincident([line2.end, line3.start])
11618  coincident([line3.end, line1.start])
11619  equalLength([line3, line1])
11620  equalLength([line1, line2])
11621  distance([line1.start, line1.end]) == 2 * x
11622}
11623
11624// Line segment with length x.
11625sketch2 = sketch(on = XY) {
11626  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11627  distance([line1.start, line1.end]) == x
11628}
11629"
11630        );
11631
11632        // Execute mock to simulate drag end.
11633        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11634        // Only the first sketch block changes.
11635        assert_eq!(
11636            src_delta.text.as_str(),
11637            "\
11638// Cube that requires the engine.
11639width = 2
11640sketch001 = startSketchOn(XY)
11641profile001 = startProfile(sketch001, at = [0, 0])
11642  |> yLine(length = width, tag = $seg1)
11643  |> xLine(length = width)
11644  |> yLine(length = -width)
11645  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11646  |> close()
11647extrude001 = extrude(profile001, length = width)
11648
11649// Get a value that requires the engine.
11650x = segLen(seg1)
11651
11652// Triangle with side length 2*x.
11653sketch(on = XY) {
11654  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11655  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11656  coincident([line1.end, line2.start])
11657  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11658  coincident([line2.end, line3.start])
11659  coincident([line3.end, line1.start])
11660  equalLength([line3, line1])
11661  equalLength([line1, line2])
11662  distance([line1.start, line1.end]) == 2 * x
11663}
11664
11665// Line segment with length x.
11666sketch2 = sketch(on = XY) {
11667  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11668  distance([line1.start, line1.end]) == x
11669}
11670"
11671        );
11672        // Exit sketch. Objects from the entire program should be present.
11673        //
11674        // - startSketchOn(XY) Plane 1
11675        // - sketch on=XY Plane 1
11676        // - Sketch block 16
11677        // - sketch on=XY cached
11678        // - Sketch block 5
11679        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11680        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11681
11682        // Edit the second sketch.
11683        //
11684        // - startSketchOn(XY) Plane 1
11685        // - sketch on=XY Plane 1
11686        // - Sketch block 16
11687        // - sketch on=XY cached
11688        // - Sketch block 5
11689        let scene_delta = frontend
11690            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11691            .await
11692            .unwrap();
11693        assert_eq!(
11694            scene_delta.new_graph.objects.len(),
11695            24,
11696            "{:#?}",
11697            scene_delta.new_graph.objects
11698        );
11699
11700        // Edit a point in the second sketch.
11701        let point_ctor = PointCtor {
11702            position: Point2d {
11703                x: Expr::Var(Number {
11704                    value: 3.0,
11705                    units: NumericSuffix::Mm,
11706                }),
11707                y: Expr::Var(Number {
11708                    value: 4.0,
11709                    units: NumericSuffix::Mm,
11710                }),
11711            },
11712        };
11713        let segments = vec![ExistingSegmentCtor {
11714            id: point2_id,
11715            ctor: SegmentCtor::Point(point_ctor),
11716        }];
11717        let (src_delta, _) = frontend
11718            .edit_segments(&mock_ctx, version, sketch2_id, segments)
11719            .await
11720            .unwrap();
11721        // Only the second sketch block changes.
11722        assert_eq!(
11723            src_delta.text.as_str(),
11724            "\
11725// Cube that requires the engine.
11726width = 2
11727sketch001 = startSketchOn(XY)
11728profile001 = startProfile(sketch001, at = [0, 0])
11729  |> yLine(length = width, tag = $seg1)
11730  |> xLine(length = width)
11731  |> yLine(length = -width)
11732  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11733  |> close()
11734extrude001 = extrude(profile001, length = width)
11735
11736// Get a value that requires the engine.
11737x = segLen(seg1)
11738
11739// Triangle with side length 2*x.
11740sketch(on = XY) {
11741  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11742  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11743  coincident([line1.end, line2.start])
11744  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11745  coincident([line2.end, line3.start])
11746  coincident([line3.end, line1.start])
11747  equalLength([line3, line1])
11748  equalLength([line1, line2])
11749  distance([line1.start, line1.end]) == 2 * x
11750}
11751
11752// Line segment with length x.
11753sketch2 = sketch(on = XY) {
11754  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11755  distance([line1.start, line1.end]) == x
11756}
11757"
11758        );
11759
11760        // Execute mock to simulate drag end.
11761        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11762        // Only the second sketch block changes.
11763        assert_eq!(
11764            src_delta.text.as_str(),
11765            "\
11766// Cube that requires the engine.
11767width = 2
11768sketch001 = startSketchOn(XY)
11769profile001 = startProfile(sketch001, at = [0, 0])
11770  |> yLine(length = width, tag = $seg1)
11771  |> xLine(length = width)
11772  |> yLine(length = -width)
11773  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11774  |> close()
11775extrude001 = extrude(profile001, length = width)
11776
11777// Get a value that requires the engine.
11778x = segLen(seg1)
11779
11780// Triangle with side length 2*x.
11781sketch(on = XY) {
11782  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11783  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11784  coincident([line1.end, line2.start])
11785  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11786  coincident([line2.end, line3.start])
11787  coincident([line3.end, line1.start])
11788  equalLength([line3, line1])
11789  equalLength([line1, line2])
11790  distance([line1.start, line1.end]) == 2 * x
11791}
11792
11793// Line segment with length x.
11794sketch2 = sketch(on = XY) {
11795  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11796  distance([line1.start, line1.end]) == x
11797}
11798"
11799        );
11800
11801        ctx.close().await;
11802        mock_ctx.close().await;
11803    }
11804
11805    #[tokio::test(flavor = "multi_thread")]
11806    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11807        clear_mem_cache().await;
11808
11809        let source = r#"sketch001 = sketch(on = XZ) {
11810  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11811}
11812sketch002 = sketch(on = XY) {
11813  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11814  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11815  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11816  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11817  coincident([line1.end, line2.start])
11818  coincident([line2.end, line3.start])
11819  coincident([line3.end, line4.start])
11820  coincident([line4.end, line1.start])
11821  parallel([line2, line4])
11822  parallel([line3, line1])
11823  perpendicular([line1, line2])
11824  horizontal(line3)
11825  coincident([line1.start, ORIGIN])
11826}
11827"#;
11828
11829        let program = Program::parse(source).unwrap().0.unwrap();
11830        let mut frontend = FrontendState::new();
11831        let ctx = ExecutorContext::new_with_engine(
11832            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11833            Default::default(),
11834        );
11835        let mock_ctx = ExecutorContext::new_mock(None).await;
11836        let version = Version(0);
11837        let project_id = ProjectId(0);
11838        let file_id = FileId(0);
11839
11840        frontend.hack_set_program(&ctx, program).await.unwrap();
11841        let sketch_objects = frontend
11842            .scene_graph
11843            .objects
11844            .iter()
11845            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11846            .collect::<Vec<_>>();
11847        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11848
11849        let sketch1_id = sketch_objects[0].id;
11850        let sketch2_id = sketch_objects[1].id;
11851
11852        frontend
11853            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11854            .await
11855            .unwrap();
11856        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11857
11858        let scene_delta = frontend
11859            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11860            .await
11861            .unwrap();
11862        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11863
11864        clear_mem_cache().await;
11865        ctx.close().await;
11866        mock_ctx.close().await;
11867    }
11868
11869    // Regression tests: operations on source code with extra whitespace/newlines.
11870    // These test that NodePath-based lookups work correctly when source ranges
11871    // are shifted by extra whitespace that wouldn't be present after formatting.
11872
11873    #[tokio::test(flavor = "multi_thread")]
11874    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11875        // Extra newlines after @settings line - this shifts all source ranges.
11876        let initial_source = "@settings(defaultLengthUnit = mm)
11877
11878
11879
11880sketch001 = sketch(on = XY) {
11881  point(at = [1in, 2in])
11882}
11883";
11884
11885        let program = Program::parse(initial_source).unwrap().0.unwrap();
11886        let mut frontend = FrontendState::new();
11887
11888        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11889        let mock_ctx = ExecutorContext::new_mock(None).await;
11890        let version = Version(0);
11891        let project_id = ProjectId(0);
11892        let file_id = FileId(0);
11893
11894        frontend.hack_set_program(&ctx, program).await.unwrap();
11895        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11896        let sketch_id = sketch_object.id;
11897
11898        // Edit sketch should succeed despite extra newlines.
11899        frontend
11900            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11901            .await
11902            .unwrap();
11903
11904        // Add a new point to the sketch.
11905        let point_ctor = PointCtor {
11906            position: Point2d {
11907                x: Expr::Number(Number {
11908                    value: 5.0,
11909                    units: NumericSuffix::Mm,
11910                }),
11911                y: Expr::Number(Number {
11912                    value: 6.0,
11913                    units: NumericSuffix::Mm,
11914                }),
11915            },
11916        };
11917        let segment = SegmentCtor::Point(point_ctor);
11918        let (src_delta, scene_delta) = frontend
11919            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11920            .await
11921            .unwrap();
11922        // After adding a point, the source should be reformatted with standard whitespace.
11923        assert!(
11924            src_delta.text.contains("point(at = [5mm, 6mm])"),
11925            "Expected new point in source, got: {}",
11926            src_delta.text
11927        );
11928        assert!(!scene_delta.new_objects.is_empty());
11929
11930        ctx.close().await;
11931        mock_ctx.close().await;
11932    }
11933
11934    #[tokio::test(flavor = "multi_thread")]
11935    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11936        // Extra newlines after @settings, with an empty sketch block.
11937        let initial_source = "@settings(defaultLengthUnit = mm)
11938
11939
11940
11941s = sketch(on = XY) {}
11942";
11943
11944        let program = Program::parse(initial_source).unwrap().0.unwrap();
11945        let mut frontend = FrontendState::new();
11946
11947        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11948        let mock_ctx = ExecutorContext::new_mock(None).await;
11949        let version = Version(0);
11950
11951        frontend.hack_set_program(&ctx, program).await.unwrap();
11952        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11953        let sketch_id = sketch_object.id;
11954
11955        let line_ctor = LineCtor {
11956            start: Point2d {
11957                x: Expr::Number(Number {
11958                    value: 0.0,
11959                    units: NumericSuffix::Mm,
11960                }),
11961                y: Expr::Number(Number {
11962                    value: 0.0,
11963                    units: NumericSuffix::Mm,
11964                }),
11965            },
11966            end: Point2d {
11967                x: Expr::Number(Number {
11968                    value: 10.0,
11969                    units: NumericSuffix::Mm,
11970                }),
11971                y: Expr::Number(Number {
11972                    value: 10.0,
11973                    units: NumericSuffix::Mm,
11974                }),
11975            },
11976            construction: None,
11977        };
11978        let segment = SegmentCtor::Line(line_ctor);
11979        let (src_delta, scene_delta) = frontend
11980            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11981            .await
11982            .unwrap();
11983        assert!(
11984            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11985            "Expected line in source, got: {}",
11986            src_delta.text
11987        );
11988        // Line creates start point, end point, and line segment.
11989        assert_eq!(scene_delta.new_objects.len(), 3);
11990
11991        ctx.close().await;
11992        mock_ctx.close().await;
11993    }
11994
11995    #[tokio::test(flavor = "multi_thread")]
11996    async fn test_extra_newlines_between_operations_edit_line() {
11997        // Extra newlines between @settings and sketch, and inside the sketch block.
11998        let initial_source = "@settings(defaultLengthUnit = mm)
11999
12000
12001sketch001 = sketch(on = XY) {
12002
12003  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12004
12005}
12006";
12007
12008        let program = Program::parse(initial_source).unwrap().0.unwrap();
12009        let mut frontend = FrontendState::new();
12010
12011        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12012        let mock_ctx = ExecutorContext::new_mock(None).await;
12013        let version = Version(0);
12014        let project_id = ProjectId(0);
12015        let file_id = FileId(0);
12016
12017        frontend.hack_set_program(&ctx, program).await.unwrap();
12018        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12019        let sketch_id = sketch_object.id;
12020        let sketch = expect_sketch(sketch_object);
12021
12022        // Extract segment IDs before edit_sketch borrows frontend mutably.
12023        let line_id = sketch
12024            .segments
12025            .iter()
12026            .copied()
12027            .find(|seg_id| {
12028                matches!(
12029                    &frontend.scene_graph.objects[seg_id.0].kind,
12030                    ObjectKind::Segment {
12031                        segment: Segment::Line(_)
12032                    }
12033                )
12034            })
12035            .expect("Expected a line segment in sketch");
12036
12037        // Enter sketch edit mode.
12038        frontend
12039            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12040            .await
12041            .unwrap();
12042
12043        // Edit the line.
12044        let line_ctor = LineCtor {
12045            start: Point2d {
12046                x: Expr::Var(Number {
12047                    value: 1.0,
12048                    units: NumericSuffix::Mm,
12049                }),
12050                y: Expr::Var(Number {
12051                    value: 2.0,
12052                    units: NumericSuffix::Mm,
12053                }),
12054            },
12055            end: Point2d {
12056                x: Expr::Var(Number {
12057                    value: 13.0,
12058                    units: NumericSuffix::Mm,
12059                }),
12060                y: Expr::Var(Number {
12061                    value: 14.0,
12062                    units: NumericSuffix::Mm,
12063                }),
12064            },
12065            construction: None,
12066        };
12067        let segments = vec![ExistingSegmentCtor {
12068            id: line_id,
12069            ctor: SegmentCtor::Line(line_ctor),
12070        }];
12071        let (src_delta, _scene_delta) = frontend
12072            .edit_segments(&mock_ctx, version, sketch_id, segments)
12073            .await
12074            .unwrap();
12075        assert!(
12076            src_delta
12077                .text
12078                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12079            "Expected edited line in source, got: {}",
12080            src_delta.text
12081        );
12082
12083        ctx.close().await;
12084        mock_ctx.close().await;
12085    }
12086
12087    #[tokio::test(flavor = "multi_thread")]
12088    async fn test_extra_newlines_delete_segment() {
12089        // Extra whitespace before and after the sketch block.
12090        let initial_source = "@settings(defaultLengthUnit = mm)
12091
12092
12093
12094sketch001 = sketch(on = XY) {
12095  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12096}
12097";
12098
12099        let program = Program::parse(initial_source).unwrap().0.unwrap();
12100        let mut frontend = FrontendState::new();
12101
12102        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12103        let mock_ctx = ExecutorContext::new_mock(None).await;
12104        let version = Version(0);
12105
12106        frontend.hack_set_program(&ctx, program).await.unwrap();
12107        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12108        let sketch_id = sketch_object.id;
12109        let sketch = expect_sketch(sketch_object);
12110
12111        // The sketch should have 3 segments: start point, center point, and the circle.
12112        assert_eq!(sketch.segments.len(), 3);
12113        let circle_id = sketch.segments[2];
12114
12115        // Delete the circle despite extra newlines in original source.
12116        let (src_delta, scene_delta) = frontend
12117            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12118            .await
12119            .unwrap();
12120        assert!(
12121            src_delta.text.contains("sketch(on = XY) {"),
12122            "Expected sketch block in source, got: {}",
12123            src_delta.text
12124        );
12125        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12126        let new_sketch = expect_sketch(new_sketch_object);
12127        assert_eq!(new_sketch.segments.len(), 0);
12128
12129        ctx.close().await;
12130        mock_ctx.close().await;
12131    }
12132
12133    #[tokio::test(flavor = "multi_thread")]
12134    async fn test_unformatted_source_add_arc() {
12135        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
12136        let initial_source = "@settings(defaultLengthUnit = mm)
12137
12138
12139
12140
12141sketch001 = sketch(on = XY) {
12142}
12143";
12144
12145        let program = Program::parse(initial_source).unwrap().0.unwrap();
12146        let mut frontend = FrontendState::new();
12147
12148        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12149        let mock_ctx = ExecutorContext::new_mock(None).await;
12150        let version = Version(0);
12151
12152        frontend.hack_set_program(&ctx, program).await.unwrap();
12153        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12154        let sketch_id = sketch_object.id;
12155
12156        let arc_ctor = ArcCtor {
12157            start: Point2d {
12158                x: Expr::Var(Number {
12159                    value: 5.0,
12160                    units: NumericSuffix::Mm,
12161                }),
12162                y: Expr::Var(Number {
12163                    value: 0.0,
12164                    units: NumericSuffix::Mm,
12165                }),
12166            },
12167            end: Point2d {
12168                x: Expr::Var(Number {
12169                    value: 0.0,
12170                    units: NumericSuffix::Mm,
12171                }),
12172                y: Expr::Var(Number {
12173                    value: 5.0,
12174                    units: NumericSuffix::Mm,
12175                }),
12176            },
12177            center: Point2d {
12178                x: Expr::Var(Number {
12179                    value: 0.0,
12180                    units: NumericSuffix::Mm,
12181                }),
12182                y: Expr::Var(Number {
12183                    value: 0.0,
12184                    units: NumericSuffix::Mm,
12185                }),
12186            },
12187            construction: None,
12188        };
12189        let segment = SegmentCtor::Arc(arc_ctor);
12190        let (src_delta, scene_delta) = frontend
12191            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12192            .await
12193            .unwrap();
12194        assert!(
12195            src_delta
12196                .text
12197                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12198            "Expected arc in source, got: {}",
12199            src_delta.text
12200        );
12201        assert!(!scene_delta.new_objects.is_empty());
12202
12203        ctx.close().await;
12204        mock_ctx.close().await;
12205    }
12206
12207    #[tokio::test(flavor = "multi_thread")]
12208    async fn test_extra_newlines_add_circle() {
12209        // Extra blank lines between settings and sketch.
12210        let initial_source = "@settings(defaultLengthUnit = mm)
12211
12212
12213
12214sketch001 = sketch(on = XY) {
12215}
12216";
12217
12218        let program = Program::parse(initial_source).unwrap().0.unwrap();
12219        let mut frontend = FrontendState::new();
12220
12221        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12222        let mock_ctx = ExecutorContext::new_mock(None).await;
12223        let version = Version(0);
12224
12225        frontend.hack_set_program(&ctx, program).await.unwrap();
12226        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12227        let sketch_id = sketch_object.id;
12228
12229        let circle_ctor = CircleCtor {
12230            start: Point2d {
12231                x: Expr::Var(Number {
12232                    value: 5.0,
12233                    units: NumericSuffix::Mm,
12234                }),
12235                y: Expr::Var(Number {
12236                    value: 0.0,
12237                    units: NumericSuffix::Mm,
12238                }),
12239            },
12240            center: Point2d {
12241                x: Expr::Var(Number {
12242                    value: 0.0,
12243                    units: NumericSuffix::Mm,
12244                }),
12245                y: Expr::Var(Number {
12246                    value: 0.0,
12247                    units: NumericSuffix::Mm,
12248                }),
12249            },
12250            construction: None,
12251        };
12252        let segment = SegmentCtor::Circle(circle_ctor);
12253        let (src_delta, scene_delta) = frontend
12254            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12255            .await
12256            .unwrap();
12257        assert!(
12258            src_delta
12259                .text
12260                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12261            "Expected circle in source, got: {}",
12262            src_delta.text
12263        );
12264        assert!(!scene_delta.new_objects.is_empty());
12265
12266        ctx.close().await;
12267        mock_ctx.close().await;
12268    }
12269
12270    #[tokio::test(flavor = "multi_thread")]
12271    async fn test_extra_newlines_add_constraint() {
12272        // Extra newlines with a sketch containing two lines - add a coincident constraint.
12273        let initial_source = "@settings(defaultLengthUnit = mm)
12274
12275
12276
12277sketch001 = sketch(on = XY) {
12278  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12279  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12280}
12281";
12282
12283        let program = Program::parse(initial_source).unwrap().0.unwrap();
12284        let mut frontend = FrontendState::new();
12285
12286        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12287        let mock_ctx = ExecutorContext::new_mock(None).await;
12288        let version = Version(0);
12289        let project_id = ProjectId(0);
12290        let file_id = FileId(0);
12291
12292        frontend.hack_set_program(&ctx, program).await.unwrap();
12293        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12294        let sketch_id = sketch_object.id;
12295        let sketch = expect_sketch(sketch_object);
12296
12297        // Extract segment data before edit_sketch borrows frontend mutably.
12298        let line_ids: Vec<ObjectId> = sketch
12299            .segments
12300            .iter()
12301            .copied()
12302            .filter(|seg_id| {
12303                matches!(
12304                    &frontend.scene_graph.objects[seg_id.0].kind,
12305                    ObjectKind::Segment {
12306                        segment: Segment::Line(_)
12307                    }
12308                )
12309            })
12310            .collect();
12311        assert_eq!(line_ids.len(), 2, "Expected two line segments");
12312
12313        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12314        let ObjectKind::Segment {
12315            segment: Segment::Line(line1_data),
12316        } = &line1.kind
12317        else {
12318            panic!("Expected line");
12319        };
12320        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12321        let ObjectKind::Segment {
12322            segment: Segment::Line(line2_data),
12323        } = &line2.kind
12324        else {
12325            panic!("Expected line");
12326        };
12327
12328        // Build constraint before entering sketch mode.
12329        let constraint = Constraint::Coincident(Coincident {
12330            segments: vec![line1_data.end.into(), line2_data.start.into()],
12331        });
12332
12333        // Enter sketch edit mode.
12334        frontend
12335            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12336            .await
12337            .unwrap();
12338        let (src_delta, _scene_delta) = frontend
12339            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12340            .await
12341            .unwrap();
12342        assert!(
12343            src_delta.text.contains("coincident("),
12344            "Expected coincident constraint in source, got: {}",
12345            src_delta.text
12346        );
12347
12348        ctx.close().await;
12349        mock_ctx.close().await;
12350    }
12351
12352    #[tokio::test(flavor = "multi_thread")]
12353    async fn test_extra_newlines_add_line_then_edit_line() {
12354        // Extra newlines after @settings - add a line, then edit it.
12355        let initial_source = "@settings(defaultLengthUnit = mm)
12356
12357
12358
12359sketch001 = sketch(on = XY) {
12360}
12361";
12362
12363        let program = Program::parse(initial_source).unwrap().0.unwrap();
12364        let mut frontend = FrontendState::new();
12365
12366        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12367        let mock_ctx = ExecutorContext::new_mock(None).await;
12368        let version = Version(0);
12369
12370        frontend.hack_set_program(&ctx, program).await.unwrap();
12371        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12372        let sketch_id = sketch_object.id;
12373
12374        // Add a line.
12375        let line_ctor = LineCtor {
12376            start: Point2d {
12377                x: Expr::Number(Number {
12378                    value: 0.0,
12379                    units: NumericSuffix::Mm,
12380                }),
12381                y: Expr::Number(Number {
12382                    value: 0.0,
12383                    units: NumericSuffix::Mm,
12384                }),
12385            },
12386            end: Point2d {
12387                x: Expr::Number(Number {
12388                    value: 10.0,
12389                    units: NumericSuffix::Mm,
12390                }),
12391                y: Expr::Number(Number {
12392                    value: 10.0,
12393                    units: NumericSuffix::Mm,
12394                }),
12395            },
12396            construction: None,
12397        };
12398        let segment = SegmentCtor::Line(line_ctor);
12399        let (src_delta, scene_delta) = frontend
12400            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12401            .await
12402            .unwrap();
12403        assert!(
12404            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12405            "Expected line in source after add, got: {}",
12406            src_delta.text
12407        );
12408        // Line creates start point, end point, and line segment.
12409        let line_id = *scene_delta.new_objects.last().unwrap();
12410
12411        // Edit the line.
12412        let line_ctor = LineCtor {
12413            start: Point2d {
12414                x: Expr::Number(Number {
12415                    value: 1.0,
12416                    units: NumericSuffix::Mm,
12417                }),
12418                y: Expr::Number(Number {
12419                    value: 2.0,
12420                    units: NumericSuffix::Mm,
12421                }),
12422            },
12423            end: Point2d {
12424                x: Expr::Number(Number {
12425                    value: 13.0,
12426                    units: NumericSuffix::Mm,
12427                }),
12428                y: Expr::Number(Number {
12429                    value: 14.0,
12430                    units: NumericSuffix::Mm,
12431                }),
12432            },
12433            construction: None,
12434        };
12435        let segments = vec![ExistingSegmentCtor {
12436            id: line_id,
12437            ctor: SegmentCtor::Line(line_ctor),
12438        }];
12439        let (src_delta, scene_delta) = frontend
12440            .edit_segments(&mock_ctx, version, sketch_id, segments)
12441            .await
12442            .unwrap();
12443        assert!(
12444            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12445            "Expected edited line in source, got: {}",
12446            src_delta.text
12447        );
12448        assert_eq!(scene_delta.new_objects, vec![]);
12449
12450        ctx.close().await;
12451        mock_ctx.close().await;
12452    }
12453}