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        additional_edited_segment_ids: Vec<ObjectId>,
1445    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1446        let sketch_block_ref =
1447            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1448
1449        let mut new_ast = self.program.ast.clone();
1450        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1451
1452        // Step 1: Edit segments (usually a single segment for tail cut)
1453        for segment in edit_segments {
1454            segment_ids_edited.insert(segment.id);
1455            match segment.ctor {
1456                SegmentCtor::Point(ctor) => self
1457                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1458                    .map_err(KclErrorWithOutputs::no_outputs)?,
1459                SegmentCtor::Line(ctor) => self
1460                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1461                    .map_err(KclErrorWithOutputs::no_outputs)?,
1462                SegmentCtor::Arc(ctor) => self
1463                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1464                    .map_err(KclErrorWithOutputs::no_outputs)?,
1465                SegmentCtor::Circle(ctor) => self
1466                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1467                    .map_err(KclErrorWithOutputs::no_outputs)?,
1468            }
1469        }
1470
1471        segment_ids_edited.extend(additional_edited_segment_ids);
1472
1473        // Step 2: Add coincident constraints
1474        for constraint in add_constraints {
1475            match constraint {
1476                Constraint::Coincident(coincident) => {
1477                    self.add_coincident(sketch, coincident, &mut new_ast)
1478                        .await
1479                        .map_err(KclErrorWithOutputs::no_outputs)?;
1480                }
1481                other => {
1482                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1483                        "unsupported constraint in tail cut batch: {other:?}"
1484                    ))));
1485                }
1486            }
1487        }
1488
1489        // Step 3: Delete constraints (if any)
1490        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1491
1492        let has_constraint_deletions = !constraint_ids_set.is_empty();
1493        for constraint_id in constraint_ids_set {
1494            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1495                .map_err(KclErrorWithOutputs::no_outputs)?;
1496        }
1497
1498        // Step 4: Single execute_after_edit
1499        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1500        // But we'll manually set invalidates_ids: true if we deleted constraints
1501        let (source_delta, mut scene_graph_delta) = self
1502            .execute_after_edit(
1503                ctx,
1504                sketch,
1505                sketch_block_ref,
1506                segment_ids_edited,
1507                EditDeleteKind::Edit,
1508                &mut new_ast,
1509            )
1510            .await?;
1511
1512        // If we deleted constraints, set invalidates_ids: true
1513        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1514        if has_constraint_deletions {
1515            scene_graph_delta.invalidates_ids = true;
1516        }
1517
1518        Ok((source_delta, scene_graph_delta))
1519    }
1520}
1521
1522impl FrontendState {
1523    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1524        self.program = program.clone();
1525
1526        // Execute so that the objects are updated and available for the next
1527        // API call.
1528        // This always uses engine execution (not mock) so that things are cached.
1529        // Engine execution now runs freedom analysis automatically.
1530        // Keep existing checkpoints alive here. History may still reference
1531        // older committed sketch states across a direct-edit boundary, and a
1532        // checkpoint restore is a full state replacement anyway. We append a
1533        // fresh baseline checkpoint after the full execution below.
1534        // Clear the freedom cache since IDs might have changed after direct editing
1535        // and we're about to run freedom analysis which will repopulate it.
1536        self.point_freedom_cache.clear();
1537        match ctx.run_with_caching(program).await {
1538            Ok(outcome) => {
1539                let outcome = self.update_state_after_exec(outcome, true);
1540                let checkpoint_id = self
1541                    .create_sketch_checkpoint(outcome.clone())
1542                    .await
1543                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1544                Ok(SetProgramOutcome::Success {
1545                    scene_graph: Box::new(self.scene_graph.clone()),
1546                    exec_outcome: Box::new(outcome),
1547                    checkpoint_id: Some(checkpoint_id),
1548                })
1549            }
1550            Err(mut err) => {
1551                // Don't return an error just because execution failed. Instead,
1552                // update state as much as possible.
1553                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1554                self.update_state_after_exec(outcome, true);
1555                err.scene_graph = Some(self.scene_graph.clone());
1556                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1557            }
1558        }
1559    }
1560
1561    /// Decorate engine execution such that our state is updated and the scene
1562    /// graph is added to the return.
1563    pub async fn engine_execute(
1564        &mut self,
1565        ctx: &ExecutorContext,
1566        program: Program,
1567    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1568        self.program = program.clone();
1569
1570        // Engine execution now runs freedom analysis automatically. Clear the
1571        // freedom cache since IDs might have changed after direct editing, and
1572        // we're about to run freedom analysis which will repopulate it.
1573        self.point_freedom_cache.clear();
1574        match ctx.run_with_caching(program).await {
1575            Ok(outcome) => {
1576                let outcome = self.update_state_after_exec(outcome, true);
1577                Ok(SceneGraphDelta {
1578                    new_graph: self.scene_graph.clone(),
1579                    exec_outcome: outcome,
1580                    // We don't know what the new objects are.
1581                    new_objects: Default::default(),
1582                    // We don't know if IDs were invalidated.
1583                    invalidates_ids: Default::default(),
1584                })
1585            }
1586            Err(mut err) => {
1587                // Update state as much as possible, even when there's an error.
1588                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1589                self.update_state_after_exec(outcome, true);
1590                err.scene_graph = Some(self.scene_graph.clone());
1591                Err(err)
1592            }
1593        }
1594    }
1595
1596    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1597        if matches!(err.error, KclError::EngineHangup { .. }) {
1598            // It's not ideal to special-case this, but this error is very
1599            // common during development, and it causes confusing downstream
1600            // errors that have nothing to do with the actual problem.
1601            return Err(err);
1602        }
1603
1604        let KclErrorWithOutputs {
1605            error,
1606            mut non_fatal,
1607            variables,
1608            #[cfg(feature = "artifact-graph")]
1609            operations,
1610            #[cfg(feature = "artifact-graph")]
1611            artifact_graph,
1612            #[cfg(feature = "artifact-graph")]
1613            scene_objects,
1614            #[cfg(feature = "artifact-graph")]
1615            source_range_to_object,
1616            #[cfg(feature = "artifact-graph")]
1617            var_solutions,
1618            filenames,
1619            default_planes,
1620            ..
1621        } = err;
1622
1623        if let Some(source_range) = error.source_ranges().first() {
1624            non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1625        } else {
1626            non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1627        }
1628
1629        Ok(ExecOutcome {
1630            variables,
1631            filenames,
1632            #[cfg(feature = "artifact-graph")]
1633            operations,
1634            #[cfg(feature = "artifact-graph")]
1635            artifact_graph,
1636            #[cfg(feature = "artifact-graph")]
1637            scene_objects,
1638            #[cfg(feature = "artifact-graph")]
1639            source_range_to_object,
1640            #[cfg(feature = "artifact-graph")]
1641            var_solutions,
1642            issues: non_fatal,
1643            default_planes,
1644        })
1645    }
1646
1647    async fn add_point(
1648        &mut self,
1649        ctx: &ExecutorContext,
1650        sketch: ObjectId,
1651        ctor: PointCtor,
1652    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1653        // Create updated KCL source from args.
1654        let at_ast = to_ast_point2d(&ctor.position)
1655            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1656        let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1657            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1658            unlabeled: None,
1659            arguments: vec![ast::LabeledArg {
1660                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1661                arg: at_ast,
1662            }],
1663            digest: None,
1664            non_code_meta: Default::default(),
1665        })));
1666
1667        // Look up existing sketch.
1668        let sketch_id = sketch;
1669        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1670            #[cfg(target_arch = "wasm32")]
1671            web_sys::console::error_1(
1672                &format!(
1673                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1674                    &self.scene_graph.objects
1675                )
1676                .into(),
1677            );
1678            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1679        })?;
1680        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1681            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1682                "Object is not a sketch, it is {}",
1683                sketch_object.kind.human_friendly_kind_with_article(),
1684            ))));
1685        };
1686        // Add the point to the AST of the sketch block.
1687        let mut new_ast = self.program.ast.clone();
1688        let (sketch_block_ref, _) = self
1689            .mutate_ast(
1690                &mut new_ast,
1691                sketch_id,
1692                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1693            )
1694            .map_err(KclErrorWithOutputs::no_outputs)?;
1695        // Convert to string source to create real source ranges.
1696        let new_source = source_from_ast(&new_ast);
1697        // Parse the new KCL source.
1698        let (new_program, errors) = Program::parse(&new_source)
1699            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1700        if !errors.is_empty() {
1701            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1702                "Error parsing KCL source after adding point: {errors:?}"
1703            ))));
1704        }
1705        let Some(new_program) = new_program else {
1706            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1707                "No AST produced after adding point".to_string(),
1708            )));
1709        };
1710
1711        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1712            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1713                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
1714            )))
1715        })?;
1716        #[cfg(not(feature = "artifact-graph"))]
1717        let _ = point_node_ref;
1718
1719        // Make sure to only set this if there are no errors.
1720        self.program = new_program.clone();
1721
1722        // Truncate after the sketch block for mock execution.
1723        let mut truncated_program = new_program;
1724        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1725            .map_err(KclErrorWithOutputs::no_outputs)?;
1726
1727        // Execute.
1728        let outcome = ctx
1729            .run_mock(
1730                &truncated_program,
1731                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1732            )
1733            .await?;
1734
1735        #[cfg(not(feature = "artifact-graph"))]
1736        let new_object_ids = Vec::new();
1737        #[cfg(feature = "artifact-graph")]
1738        let new_object_ids = {
1739            let make_err =
1740                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1741            let segment_id = outcome
1742                .source_range_to_object
1743                .get(&point_node_ref.range)
1744                .copied()
1745                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
1746            let segment_object = outcome
1747                .scene_objects
1748                .get(segment_id.0)
1749                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1750            let ObjectKind::Segment { segment } = &segment_object.kind else {
1751                return Err(make_err(format!(
1752                    "Object is not a segment, it is {}",
1753                    segment_object.kind.human_friendly_kind_with_article()
1754                )));
1755            };
1756            let Segment::Point(_) = segment else {
1757                return Err(make_err(format!(
1758                    "Segment is not a point, it is {}",
1759                    segment.human_friendly_kind_with_article()
1760                )));
1761            };
1762            vec![segment_id]
1763        };
1764        let src_delta = SourceDelta { text: new_source };
1765        // Uses .no_freedom_analysis() so freedom_analysis: false
1766        let outcome = self.update_state_after_exec(outcome, false);
1767        let scene_graph_delta = SceneGraphDelta {
1768            new_graph: self.scene_graph.clone(),
1769            invalidates_ids: false,
1770            new_objects: new_object_ids,
1771            exec_outcome: outcome,
1772        };
1773        Ok((src_delta, scene_graph_delta))
1774    }
1775
1776    async fn add_line(
1777        &mut self,
1778        ctx: &ExecutorContext,
1779        sketch: ObjectId,
1780        ctor: LineCtor,
1781    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1782        // Create updated KCL source from args.
1783        let start_ast = to_ast_point2d(&ctor.start)
1784            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1785        let end_ast = to_ast_point2d(&ctor.end)
1786            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1787        let mut arguments = vec![
1788            ast::LabeledArg {
1789                label: Some(ast::Identifier::new(LINE_START_PARAM)),
1790                arg: start_ast,
1791            },
1792            ast::LabeledArg {
1793                label: Some(ast::Identifier::new(LINE_END_PARAM)),
1794                arg: end_ast,
1795            },
1796        ];
1797        // Add construction kwarg if construction is Some(true)
1798        if ctor.construction == Some(true) {
1799            arguments.push(ast::LabeledArg {
1800                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1801                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1802                    value: ast::LiteralValue::Bool(true),
1803                    raw: "true".to_string(),
1804                    digest: None,
1805                }))),
1806            });
1807        }
1808        let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1809            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1810            unlabeled: None,
1811            arguments,
1812            digest: None,
1813            non_code_meta: Default::default(),
1814        })));
1815
1816        // Look up existing sketch.
1817        let sketch_id = sketch;
1818        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1819            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1820        })?;
1821        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1822            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1823                "Object is not a sketch, it is {}",
1824                sketch_object.kind.human_friendly_kind_with_article(),
1825            ))));
1826        };
1827        // Add the line to the AST of the sketch block.
1828        let mut new_ast = self.program.ast.clone();
1829        let (sketch_block_ref, _) = self
1830            .mutate_ast(
1831                &mut new_ast,
1832                sketch_id,
1833                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1834            )
1835            .map_err(KclErrorWithOutputs::no_outputs)?;
1836        // Convert to string source to create real source ranges.
1837        let new_source = source_from_ast(&new_ast);
1838        // Parse the new KCL source.
1839        let (new_program, errors) = Program::parse(&new_source)
1840            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1841        if !errors.is_empty() {
1842            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1843                "Error parsing KCL source after adding line: {errors:?}"
1844            ))));
1845        }
1846        let Some(new_program) = new_program else {
1847            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1848                "No AST produced after adding line".to_string(),
1849            )));
1850        };
1851
1852        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1853            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1854                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
1855            )))
1856        })?;
1857        #[cfg(not(feature = "artifact-graph"))]
1858        let _ = line_node_ref;
1859
1860        // Make sure to only set this if there are no errors.
1861        self.program = new_program.clone();
1862
1863        // Truncate after the sketch block for mock execution.
1864        let mut truncated_program = new_program;
1865        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
1866            .map_err(KclErrorWithOutputs::no_outputs)?;
1867
1868        // Execute.
1869        let outcome = ctx
1870            .run_mock(
1871                &truncated_program,
1872                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1873            )
1874            .await?;
1875
1876        #[cfg(not(feature = "artifact-graph"))]
1877        let new_object_ids = Vec::new();
1878        #[cfg(feature = "artifact-graph")]
1879        let new_object_ids = {
1880            let make_err =
1881                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
1882            let segment_id = outcome
1883                .source_range_to_object
1884                .get(&line_node_ref.range)
1885                .copied()
1886                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
1887            let segment_object = outcome
1888                .scene_object_by_id(segment_id)
1889                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
1890            let ObjectKind::Segment { segment } = &segment_object.kind else {
1891                return Err(make_err(format!(
1892                    "Object is not a segment, it is {}",
1893                    segment_object.kind.human_friendly_kind_with_article()
1894                )));
1895            };
1896            let Segment::Line(line) = segment else {
1897                return Err(make_err(format!(
1898                    "Segment is not a line, it is {}",
1899                    segment.human_friendly_kind_with_article()
1900                )));
1901            };
1902            vec![line.start, line.end, segment_id]
1903        };
1904        let src_delta = SourceDelta { text: new_source };
1905        // Uses .no_freedom_analysis() so freedom_analysis: false
1906        let outcome = self.update_state_after_exec(outcome, false);
1907        let scene_graph_delta = SceneGraphDelta {
1908            new_graph: self.scene_graph.clone(),
1909            invalidates_ids: false,
1910            new_objects: new_object_ids,
1911            exec_outcome: outcome,
1912        };
1913        Ok((src_delta, scene_graph_delta))
1914    }
1915
1916    async fn add_arc(
1917        &mut self,
1918        ctx: &ExecutorContext,
1919        sketch: ObjectId,
1920        ctor: ArcCtor,
1921    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1922        // Create updated KCL source from args.
1923        let start_ast = to_ast_point2d(&ctor.start)
1924            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1925        let end_ast = to_ast_point2d(&ctor.end)
1926            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1927        let center_ast = to_ast_point2d(&ctor.center)
1928            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1929        let mut arguments = vec![
1930            ast::LabeledArg {
1931                label: Some(ast::Identifier::new(ARC_START_PARAM)),
1932                arg: start_ast,
1933            },
1934            ast::LabeledArg {
1935                label: Some(ast::Identifier::new(ARC_END_PARAM)),
1936                arg: end_ast,
1937            },
1938            ast::LabeledArg {
1939                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1940                arg: center_ast,
1941            },
1942        ];
1943        // Add construction kwarg if construction is Some(true)
1944        if ctor.construction == Some(true) {
1945            arguments.push(ast::LabeledArg {
1946                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1947                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1948                    value: ast::LiteralValue::Bool(true),
1949                    raw: "true".to_string(),
1950                    digest: None,
1951                }))),
1952            });
1953        }
1954        let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1955            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1956            unlabeled: None,
1957            arguments,
1958            digest: None,
1959            non_code_meta: Default::default(),
1960        })));
1961
1962        // Look up existing sketch.
1963        let sketch_id = sketch;
1964        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1965            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1966        })?;
1967        let ObjectKind::Sketch(_) = &sketch_object.kind else {
1968            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1969                "Object is not a sketch, it is {}",
1970                sketch_object.kind.human_friendly_kind_with_article(),
1971            ))));
1972        };
1973        // Add the arc to the AST of the sketch block.
1974        let mut new_ast = self.program.ast.clone();
1975        let (sketch_block_ref, _) = self
1976            .mutate_ast(
1977                &mut new_ast,
1978                sketch_id,
1979                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1980            )
1981            .map_err(KclErrorWithOutputs::no_outputs)?;
1982        // Convert to string source to create real source ranges.
1983        let new_source = source_from_ast(&new_ast);
1984        // Parse the new KCL source.
1985        let (new_program, errors) = Program::parse(&new_source)
1986            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1987        if !errors.is_empty() {
1988            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1989                "Error parsing KCL source after adding arc: {errors:?}"
1990            ))));
1991        }
1992        let Some(new_program) = new_program else {
1993            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1994                "No AST produced after adding arc".to_string(),
1995            )));
1996        };
1997
1998        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
1999            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2000                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2001            )))
2002        })?;
2003        #[cfg(not(feature = "artifact-graph"))]
2004        let _ = arc_node_ref;
2005
2006        // Make sure to only set this if there are no errors.
2007        self.program = new_program.clone();
2008
2009        // Truncate after the sketch block for mock execution.
2010        let mut truncated_program = new_program;
2011        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2012            .map_err(KclErrorWithOutputs::no_outputs)?;
2013
2014        // Execute.
2015        let outcome = ctx
2016            .run_mock(
2017                &truncated_program,
2018                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2019            )
2020            .await?;
2021
2022        #[cfg(not(feature = "artifact-graph"))]
2023        let new_object_ids = Vec::new();
2024        #[cfg(feature = "artifact-graph")]
2025        let new_object_ids = {
2026            let make_err =
2027                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2028            let segment_id = outcome
2029                .source_range_to_object
2030                .get(&arc_node_ref.range)
2031                .copied()
2032                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2033            let segment_object = outcome
2034                .scene_objects
2035                .get(segment_id.0)
2036                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2037            let ObjectKind::Segment { segment } = &segment_object.kind else {
2038                return Err(make_err(format!(
2039                    "Object is not a segment, it is {}",
2040                    segment_object.kind.human_friendly_kind_with_article()
2041                )));
2042            };
2043            let Segment::Arc(arc) = segment else {
2044                return Err(make_err(format!(
2045                    "Segment is not an arc, it is {}",
2046                    segment.human_friendly_kind_with_article()
2047                )));
2048            };
2049            vec![arc.start, arc.end, arc.center, segment_id]
2050        };
2051        let src_delta = SourceDelta { text: new_source };
2052        // Uses .no_freedom_analysis() so freedom_analysis: false
2053        let outcome = self.update_state_after_exec(outcome, false);
2054        let scene_graph_delta = SceneGraphDelta {
2055            new_graph: self.scene_graph.clone(),
2056            invalidates_ids: false,
2057            new_objects: new_object_ids,
2058            exec_outcome: outcome,
2059        };
2060        Ok((src_delta, scene_graph_delta))
2061    }
2062
2063    async fn add_circle(
2064        &mut self,
2065        ctx: &ExecutorContext,
2066        sketch: ObjectId,
2067        ctor: CircleCtor,
2068    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2069        // Create updated KCL source from args.
2070        let start_ast = to_ast_point2d(&ctor.start)
2071            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2072        let center_ast = to_ast_point2d(&ctor.center)
2073            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2074        let mut arguments = vec![
2075            ast::LabeledArg {
2076                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2077                arg: start_ast,
2078            },
2079            ast::LabeledArg {
2080                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2081                arg: center_ast,
2082            },
2083        ];
2084        // Add construction kwarg if construction is Some(true)
2085        if ctor.construction == Some(true) {
2086            arguments.push(ast::LabeledArg {
2087                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2088                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2089                    value: ast::LiteralValue::Bool(true),
2090                    raw: "true".to_string(),
2091                    digest: None,
2092                }))),
2093            });
2094        }
2095        let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2096            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2097            unlabeled: None,
2098            arguments,
2099            digest: None,
2100            non_code_meta: Default::default(),
2101        })));
2102
2103        // Look up existing sketch.
2104        let sketch_id = sketch;
2105        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2106            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2107        })?;
2108        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2109            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2110                "Object is not a sketch, it is {}",
2111                sketch_object.kind.human_friendly_kind_with_article(),
2112            ))));
2113        };
2114        // Add the circle to the AST of the sketch block.
2115        let mut new_ast = self.program.ast.clone();
2116        let (sketch_block_ref, _) = self
2117            .mutate_ast(
2118                &mut new_ast,
2119                sketch_id,
2120                AstMutateCommand::AddSketchBlockVarDecl {
2121                    prefix: CIRCLE_VARIABLE.to_owned(),
2122                    expr: circle_ast,
2123                },
2124            )
2125            .map_err(KclErrorWithOutputs::no_outputs)?;
2126        // Convert to string source to create real source ranges.
2127        let new_source = source_from_ast(&new_ast);
2128        // Parse the new KCL source.
2129        let (new_program, errors) = Program::parse(&new_source)
2130            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2131        if !errors.is_empty() {
2132            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2133                "Error parsing KCL source after adding circle: {errors:?}"
2134            ))));
2135        }
2136        let Some(new_program) = new_program else {
2137            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2138                "No AST produced after adding circle".to_string(),
2139            )));
2140        };
2141
2142        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2143            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2144                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2145            )))
2146        })?;
2147        #[cfg(not(feature = "artifact-graph"))]
2148        let _ = circle_node_ref;
2149
2150        // Make sure to only set this if there are no errors.
2151        self.program = new_program.clone();
2152
2153        // Truncate after the sketch block for mock execution.
2154        let mut truncated_program = new_program;
2155        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2156            .map_err(KclErrorWithOutputs::no_outputs)?;
2157
2158        // Execute.
2159        let outcome = ctx
2160            .run_mock(
2161                &truncated_program,
2162                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2163            )
2164            .await?;
2165
2166        #[cfg(not(feature = "artifact-graph"))]
2167        let new_object_ids = Vec::new();
2168        #[cfg(feature = "artifact-graph")]
2169        let new_object_ids = {
2170            let make_err =
2171                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2172            let segment_id = outcome
2173                .source_range_to_object
2174                .get(&circle_node_ref.range)
2175                .copied()
2176                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2177            let segment_object = outcome
2178                .scene_objects
2179                .get(segment_id.0)
2180                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2181            let ObjectKind::Segment { segment } = &segment_object.kind else {
2182                return Err(make_err(format!(
2183                    "Object is not a segment, it is {}",
2184                    segment_object.kind.human_friendly_kind_with_article()
2185                )));
2186            };
2187            let Segment::Circle(circle) = segment else {
2188                return Err(make_err(format!(
2189                    "Segment is not a circle, it is {}",
2190                    segment.human_friendly_kind_with_article()
2191                )));
2192            };
2193            vec![circle.start, circle.center, segment_id]
2194        };
2195        let src_delta = SourceDelta { text: new_source };
2196        // Uses .no_freedom_analysis() so freedom_analysis: false
2197        let outcome = self.update_state_after_exec(outcome, false);
2198        let scene_graph_delta = SceneGraphDelta {
2199            new_graph: self.scene_graph.clone(),
2200            invalidates_ids: false,
2201            new_objects: new_object_ids,
2202            exec_outcome: outcome,
2203        };
2204        Ok((src_delta, scene_graph_delta))
2205    }
2206
2207    fn edit_point(
2208        &mut self,
2209        new_ast: &mut ast::Node<ast::Program>,
2210        sketch: ObjectId,
2211        point: ObjectId,
2212        ctor: PointCtor,
2213    ) -> Result<(), KclError> {
2214        // Create updated KCL source from args.
2215        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2216
2217        // Look up existing sketch.
2218        let sketch_id = sketch;
2219        let sketch_object = self
2220            .scene_graph
2221            .objects
2222            .get(sketch_id.0)
2223            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2224        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2225            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2226        };
2227        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2228            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2229        })?;
2230        // Look up existing point.
2231        let point_id = point;
2232        let point_object = self
2233            .scene_graph
2234            .objects
2235            .get(point_id.0)
2236            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2237        let ObjectKind::Segment {
2238            segment: Segment::Point(point),
2239        } = &point_object.kind
2240        else {
2241            return Err(KclError::refactor(format!(
2242                "Object is not a point segment: {point_object:?}"
2243            )));
2244        };
2245
2246        // If the point is part of a line or arc, edit the line/arc instead.
2247        if let Some(owner_id) = point.owner {
2248            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2249                KclError::refactor(format!(
2250                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2251                ))
2252            })?;
2253            let ObjectKind::Segment { segment } = &owner_object.kind else {
2254                return Err(KclError::refactor(format!(
2255                    "Internal: Owner of point is not a segment, but found {}",
2256                    owner_object.kind.human_friendly_kind_with_article()
2257                )));
2258            };
2259
2260            // Handle Line owner
2261            if let Segment::Line(line) = segment {
2262                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2263                    return Err(KclError::refactor(format!(
2264                        "Internal: Owner of point does not have line ctor, but found {}",
2265                        line.ctor.human_friendly_kind_with_article()
2266                    )));
2267                };
2268                let mut line_ctor = line_ctor.clone();
2269                // Which end of the line is this point?
2270                if line.start == point_id {
2271                    line_ctor.start = ctor.position;
2272                } else if line.end == point_id {
2273                    line_ctor.end = ctor.position;
2274                } else {
2275                    return Err(KclError::refactor(format!(
2276                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2277                    )));
2278                }
2279                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2280            }
2281
2282            // Handle Arc owner
2283            if let Segment::Arc(arc) = segment {
2284                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2285                    return Err(KclError::refactor(format!(
2286                        "Internal: Owner of point does not have arc ctor, but found {}",
2287                        arc.ctor.human_friendly_kind_with_article()
2288                    )));
2289                };
2290                let mut arc_ctor = arc_ctor.clone();
2291                // Which point of the arc is this? (center, start, or end)
2292                if arc.center == point_id {
2293                    arc_ctor.center = ctor.position;
2294                } else if arc.start == point_id {
2295                    arc_ctor.start = ctor.position;
2296                } else if arc.end == point_id {
2297                    arc_ctor.end = ctor.position;
2298                } else {
2299                    return Err(KclError::refactor(format!(
2300                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2301                    )));
2302                }
2303                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2304            }
2305
2306            // Handle Circle owner
2307            if let Segment::Circle(circle) = segment {
2308                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2309                    return Err(KclError::refactor(format!(
2310                        "Internal: Owner of point does not have circle ctor, but found {}",
2311                        circle.ctor.human_friendly_kind_with_article()
2312                    )));
2313                };
2314                let mut circle_ctor = circle_ctor.clone();
2315                if circle.center == point_id {
2316                    circle_ctor.center = ctor.position;
2317                } else if circle.start == point_id {
2318                    circle_ctor.start = ctor.position;
2319                } else {
2320                    return Err(KclError::refactor(format!(
2321                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2322                    )));
2323                }
2324                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2325            }
2326
2327            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2328            // (fall through to the point editing logic below)
2329        }
2330
2331        // Modify the point AST.
2332        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2333        Ok(())
2334    }
2335
2336    fn edit_line(
2337        &mut self,
2338        new_ast: &mut ast::Node<ast::Program>,
2339        sketch: ObjectId,
2340        line: ObjectId,
2341        ctor: LineCtor,
2342    ) -> Result<(), KclError> {
2343        // Create updated KCL source from args.
2344        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2345        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2346
2347        // Look up existing sketch.
2348        let sketch_id = sketch;
2349        let sketch_object = self
2350            .scene_graph
2351            .objects
2352            .get(sketch_id.0)
2353            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2354        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2355            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2356        };
2357        sketch
2358            .segments
2359            .iter()
2360            .find(|o| **o == line)
2361            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2362        // Look up existing line.
2363        let line_id = line;
2364        let line_object = self
2365            .scene_graph
2366            .objects
2367            .get(line_id.0)
2368            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2369        let ObjectKind::Segment { .. } = &line_object.kind else {
2370            let kind = line_object.kind.human_friendly_kind_with_article();
2371            return Err(KclError::refactor(format!(
2372                "This constraint only works on Segments, but you selected {kind}"
2373            )));
2374        };
2375
2376        // Modify the line AST.
2377        self.mutate_ast(
2378            new_ast,
2379            line_id,
2380            AstMutateCommand::EditLine {
2381                start: new_start_ast,
2382                end: new_end_ast,
2383                construction: ctor.construction,
2384            },
2385        )?;
2386        Ok(())
2387    }
2388
2389    fn edit_arc(
2390        &mut self,
2391        new_ast: &mut ast::Node<ast::Program>,
2392        sketch: ObjectId,
2393        arc: ObjectId,
2394        ctor: ArcCtor,
2395    ) -> Result<(), KclError> {
2396        // Create updated KCL source from args.
2397        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2398        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2399        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2400
2401        // Look up existing sketch.
2402        let sketch_id = sketch;
2403        let sketch_object = self
2404            .scene_graph
2405            .objects
2406            .get(sketch_id.0)
2407            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2408        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2409            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2410        };
2411        sketch
2412            .segments
2413            .iter()
2414            .find(|o| **o == arc)
2415            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2416        // Look up existing arc.
2417        let arc_id = arc;
2418        let arc_object = self
2419            .scene_graph
2420            .objects
2421            .get(arc_id.0)
2422            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2423        let ObjectKind::Segment { .. } = &arc_object.kind else {
2424            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2425        };
2426
2427        // Modify the arc AST.
2428        self.mutate_ast(
2429            new_ast,
2430            arc_id,
2431            AstMutateCommand::EditArc {
2432                start: new_start_ast,
2433                end: new_end_ast,
2434                center: new_center_ast,
2435                construction: ctor.construction,
2436            },
2437        )?;
2438        Ok(())
2439    }
2440
2441    fn edit_circle(
2442        &mut self,
2443        new_ast: &mut ast::Node<ast::Program>,
2444        sketch: ObjectId,
2445        circle: ObjectId,
2446        ctor: CircleCtor,
2447    ) -> Result<(), KclError> {
2448        // Create updated KCL source from args.
2449        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2450        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2451
2452        // Look up existing sketch.
2453        let sketch_id = sketch;
2454        let sketch_object = self
2455            .scene_graph
2456            .objects
2457            .get(sketch_id.0)
2458            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2459        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2460            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2461        };
2462        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2463            KclError::refactor(format!(
2464                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2465            ))
2466        })?;
2467        // Look up existing circle.
2468        let circle_id = circle;
2469        let circle_object = self
2470            .scene_graph
2471            .objects
2472            .get(circle_id.0)
2473            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2474        let ObjectKind::Segment { .. } = &circle_object.kind else {
2475            return Err(KclError::refactor(format!(
2476                "Object is not a segment: {circle_object:?}"
2477            )));
2478        };
2479
2480        // Modify the circle AST.
2481        self.mutate_ast(
2482            new_ast,
2483            circle_id,
2484            AstMutateCommand::EditCircle {
2485                start: new_start_ast,
2486                center: new_center_ast,
2487                construction: ctor.construction,
2488            },
2489        )?;
2490        Ok(())
2491    }
2492
2493    fn delete_segment(
2494        &mut self,
2495        new_ast: &mut ast::Node<ast::Program>,
2496        sketch: ObjectId,
2497        segment_id: ObjectId,
2498    ) -> Result<(), KclError> {
2499        // Look up existing sketch.
2500        let sketch_id = sketch;
2501        let sketch_object = self
2502            .scene_graph
2503            .objects
2504            .get(sketch_id.0)
2505            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2506        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2507            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2508        };
2509        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2510            KclError::refactor(format!(
2511                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2512            ))
2513        })?;
2514        // Look up existing segment.
2515        let segment_object =
2516            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2517                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2518            })?;
2519        let ObjectKind::Segment { .. } = &segment_object.kind else {
2520            return Err(KclError::refactor(format!(
2521                "Object is not a segment, it is {}",
2522                segment_object.kind.human_friendly_kind_with_article()
2523            )));
2524        };
2525
2526        // Modify the AST to remove the segment.
2527        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
2528        Ok(())
2529    }
2530
2531    fn delete_constraint(
2532        &mut self,
2533        new_ast: &mut ast::Node<ast::Program>,
2534        sketch: ObjectId,
2535        constraint_id: ObjectId,
2536    ) -> Result<(), KclError> {
2537        // Look up existing sketch.
2538        let sketch_id = sketch;
2539        let sketch_object = self
2540            .scene_graph
2541            .objects
2542            .get(sketch_id.0)
2543            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2544        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2545            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2546        };
2547        sketch
2548            .constraints
2549            .iter()
2550            .find(|o| **o == constraint_id)
2551            .ok_or_else(|| {
2552                KclError::refactor(format!(
2553                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
2554                ))
2555            })?;
2556        // Look up existing constraint.
2557        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
2558            KclError::refactor(format!(
2559                "Constraint not found in scene graph: constraint={constraint_id:?}"
2560            ))
2561        })?;
2562        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
2563            return Err(KclError::refactor(format!(
2564                "Object is not a constraint, it is {}",
2565                constraint_object.kind.human_friendly_kind_with_article()
2566            )));
2567        };
2568
2569        // Modify the AST to remove the constraint.
2570        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
2571        Ok(())
2572    }
2573
2574    fn edit_coincident_constraint(
2575        &mut self,
2576        new_ast: &mut ast::Node<ast::Program>,
2577        constraint_id: ObjectId,
2578        segments: Vec<ConstraintSegment>,
2579    ) -> Result<(), KclError> {
2580        if segments.len() < 2 {
2581            return Err(KclError::refactor(format!(
2582                "Coincident constraint must have at least 2 inputs, got {}",
2583                segments.len()
2584            )));
2585        }
2586
2587        let segment_asts = segments
2588            .iter()
2589            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
2590            .collect::<Result<Vec<_>, _>>()?;
2591
2592        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2593            elements: segment_asts,
2594            digest: None,
2595            non_code_meta: Default::default(),
2596        })));
2597
2598        self.mutate_ast(
2599            new_ast,
2600            constraint_id,
2601            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2602        )?;
2603        Ok(())
2604    }
2605
2606    fn edit_horizontal_points_constraint(
2607        &mut self,
2608        new_ast: &mut ast::Node<ast::Program>,
2609        constraint_id: ObjectId,
2610        points: Vec<ConstraintSegment>,
2611    ) -> Result<(), KclError> {
2612        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
2613    }
2614
2615    fn edit_vertical_points_constraint(
2616        &mut self,
2617        new_ast: &mut ast::Node<ast::Program>,
2618        constraint_id: ObjectId,
2619        points: Vec<ConstraintSegment>,
2620    ) -> Result<(), KclError> {
2621        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
2622    }
2623
2624    fn edit_axis_points_constraint(
2625        &mut self,
2626        new_ast: &mut ast::Node<ast::Program>,
2627        constraint_id: ObjectId,
2628        points: Vec<ConstraintSegment>,
2629        constraint_name: &str,
2630    ) -> Result<(), KclError> {
2631        if points.len() < 2 {
2632            return Err(KclError::refactor(format!(
2633                "{constraint_name} points constraint must have at least 2 points, got {}",
2634                points.len()
2635            )));
2636        }
2637
2638        let point_asts = points
2639            .iter()
2640            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
2641            .collect::<Result<Vec<_>, _>>()?;
2642
2643        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2644            elements: point_asts,
2645            digest: None,
2646            non_code_meta: Default::default(),
2647        })));
2648
2649        self.mutate_ast(
2650            new_ast,
2651            constraint_id,
2652            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2653        )?;
2654        Ok(())
2655    }
2656
2657    /// updates the equalLength constraint with the given lines
2658    fn edit_equal_length_constraint(
2659        &mut self,
2660        new_ast: &mut ast::Node<ast::Program>,
2661        constraint_id: ObjectId,
2662        lines: Vec<ObjectId>,
2663    ) -> Result<(), KclError> {
2664        if lines.len() < 2 {
2665            return Err(KclError::refactor(format!(
2666                "Lines equal length constraint must have at least 2 lines, got {}",
2667                lines.len()
2668            )));
2669        }
2670
2671        let line_asts = lines
2672            .iter()
2673            .map(|line_id| {
2674                let line_object = self
2675                    .scene_graph
2676                    .objects
2677                    .get(line_id.0)
2678                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2679                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2680                    let kind = line_object.kind.human_friendly_kind_with_article();
2681                    return Err(KclError::refactor(format!(
2682                        "This constraint only works on Segments, but you selected {kind}"
2683                    )));
2684                };
2685                let Segment::Line(_) = line_segment else {
2686                    let kind = line_segment.human_friendly_kind_with_article();
2687                    return Err(KclError::refactor(format!(
2688                        "Only lines can be made equal length, but you selected {kind}"
2689                    )));
2690                };
2691
2692                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2693            })
2694            .collect::<Result<Vec<_>, _>>()?;
2695
2696        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2697            elements: line_asts,
2698            digest: None,
2699            non_code_meta: Default::default(),
2700        })));
2701
2702        self.mutate_ast(
2703            new_ast,
2704            constraint_id,
2705            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2706        )?;
2707        Ok(())
2708    }
2709
2710    /// Updates the parallel constraint with the given lines.
2711    fn edit_parallel_constraint(
2712        &mut self,
2713        new_ast: &mut ast::Node<ast::Program>,
2714        constraint_id: ObjectId,
2715        lines: Vec<ObjectId>,
2716    ) -> Result<(), KclError> {
2717        if lines.len() < 2 {
2718            return Err(KclError::refactor(format!(
2719                "Parallel constraint must have at least 2 lines, got {}",
2720                lines.len()
2721            )));
2722        }
2723
2724        let line_asts = lines
2725            .iter()
2726            .map(|line_id| {
2727                let line_object = self
2728                    .scene_graph
2729                    .objects
2730                    .get(line_id.0)
2731                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
2732                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2733                    let kind = line_object.kind.human_friendly_kind_with_article();
2734                    return Err(KclError::refactor(format!(
2735                        "This constraint only works on Segments, but you selected {kind}"
2736                    )));
2737                };
2738                let Segment::Line(_) = line_segment else {
2739                    let kind = line_segment.human_friendly_kind_with_article();
2740                    return Err(KclError::refactor(format!(
2741                        "Only lines can be made parallel, but you selected {kind}"
2742                    )));
2743                };
2744
2745                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
2746            })
2747            .collect::<Result<Vec<_>, _>>()?;
2748
2749        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2750            elements: line_asts,
2751            digest: None,
2752            non_code_meta: Default::default(),
2753        })));
2754
2755        self.mutate_ast(
2756            new_ast,
2757            constraint_id,
2758            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2759        )?;
2760        Ok(())
2761    }
2762
2763    /// Updates the equalRadius constraint with the given segments.
2764    fn edit_equal_radius_constraint(
2765        &mut self,
2766        new_ast: &mut ast::Node<ast::Program>,
2767        constraint_id: ObjectId,
2768        input: Vec<ObjectId>,
2769    ) -> Result<(), KclError> {
2770        if input.len() < 2 {
2771            return Err(KclError::refactor(format!(
2772                "equalRadius constraint must have at least 2 segments, got {}",
2773                input.len()
2774            )));
2775        }
2776
2777        let input_asts = input
2778            .iter()
2779            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
2780            .collect::<Result<Vec<_>, _>>()?;
2781
2782        let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
2783            elements: input_asts,
2784            digest: None,
2785            non_code_meta: Default::default(),
2786        })));
2787
2788        self.mutate_ast(
2789            new_ast,
2790            constraint_id,
2791            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
2792        )?;
2793        Ok(())
2794    }
2795
2796    async fn execute_after_edit(
2797        &mut self,
2798        ctx: &ExecutorContext,
2799        sketch: ObjectId,
2800        sketch_block_ref: AstNodeRef,
2801        segment_ids_edited: AhashIndexSet<ObjectId>,
2802        edit_kind: EditDeleteKind,
2803        new_ast: &mut ast::Node<ast::Program>,
2804    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2805        // Convert to string source to create real source ranges.
2806        let new_source = source_from_ast(new_ast);
2807        // Parse the new KCL source.
2808        let (new_program, errors) = Program::parse(&new_source)
2809            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2810        if !errors.is_empty() {
2811            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2812                "Error parsing KCL source after editing: {errors:?}"
2813            ))));
2814        }
2815        let Some(new_program) = new_program else {
2816            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2817                "No AST produced after editing".to_string(),
2818            )));
2819        };
2820
2821        // TODO: sketch-api: make sure to only set this if there are no errors.
2822        self.program = new_program.clone();
2823
2824        // Truncate after the sketch block for mock execution.
2825        let is_delete = edit_kind.is_delete();
2826        let truncated_program = {
2827            let mut truncated_program = new_program;
2828            only_sketch_block(
2829                &mut truncated_program.ast,
2830                &sketch_block_ref,
2831                edit_kind.to_change_kind(),
2832            )
2833            .map_err(KclErrorWithOutputs::no_outputs)?;
2834            truncated_program
2835        };
2836
2837        #[cfg(not(feature = "artifact-graph"))]
2838        drop(segment_ids_edited);
2839
2840        // Execute.
2841        let mock_config = MockConfig {
2842            sketch_block_id: Some(sketch),
2843            freedom_analysis: is_delete,
2844            #[cfg(feature = "artifact-graph")]
2845            segment_ids_edited: segment_ids_edited.clone(),
2846            ..Default::default()
2847        };
2848        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
2849
2850        // Uses freedom_analysis: is_delete
2851        let outcome = self.update_state_after_exec(outcome, is_delete);
2852
2853        #[cfg(feature = "artifact-graph")]
2854        let new_source = {
2855            // Feed back sketch var solutions into the source.
2856            //
2857            // The interpreter is returning all var solutions from the sketch
2858            // block we're editing.
2859            let mut new_ast = self.program.ast.clone();
2860            for (var_range, value) in &outcome.var_solutions {
2861                let rounded = value.round(3);
2862                let source_ref = SourceRef::Simple {
2863                    range: *var_range,
2864                    node_path: None,
2865                };
2866                mutate_ast_node_by_source_ref(
2867                    &mut new_ast,
2868                    &source_ref,
2869                    AstMutateCommand::EditVarInitialValue { value: rounded },
2870                )
2871                .map_err(|err| KclErrorWithOutputs::from_error_outcome(err, outcome.clone()))?;
2872            }
2873            source_from_ast(&new_ast)
2874        };
2875
2876        let src_delta = SourceDelta { text: new_source };
2877        let scene_graph_delta = SceneGraphDelta {
2878            new_graph: self.scene_graph.clone(),
2879            invalidates_ids: is_delete,
2880            new_objects: Vec::new(),
2881            exec_outcome: outcome,
2882        };
2883        Ok((src_delta, scene_graph_delta))
2884    }
2885
2886    async fn execute_after_delete_sketch(
2887        &mut self,
2888        ctx: &ExecutorContext,
2889        new_ast: &mut ast::Node<ast::Program>,
2890    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2891        // Convert to string source to create real source ranges.
2892        let new_source = source_from_ast(new_ast);
2893        // Parse the new KCL source.
2894        let (new_program, errors) = Program::parse(&new_source)
2895            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2896        if !errors.is_empty() {
2897            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2898                "Error parsing KCL source after editing: {errors:?}"
2899            ))));
2900        }
2901        let Some(new_program) = new_program else {
2902            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2903                "No AST produced after editing".to_string(),
2904            )));
2905        };
2906
2907        // Make sure to only set this if there are no errors.
2908        self.program = new_program.clone();
2909
2910        // We deleted the entire sketch block. It doesn't make sense to truncate
2911        // and execute only the sketch block. We execute the whole program with
2912        // a real engine.
2913
2914        // Execute.
2915        let outcome = ctx.run_with_caching(new_program).await?;
2916        let freedom_analysis_ran = true;
2917
2918        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
2919
2920        let src_delta = SourceDelta { text: new_source };
2921        let scene_graph_delta = SceneGraphDelta {
2922            new_graph: self.scene_graph.clone(),
2923            invalidates_ids: true,
2924            new_objects: Vec::new(),
2925            exec_outcome: outcome,
2926        };
2927        Ok((src_delta, scene_graph_delta))
2928    }
2929
2930    /// Map a point object id into an AST reference expression for use in
2931    /// constraints. If the point is owned by a segment (line or arc), we
2932    /// reference the appropriate property on that segment (e.g. `line1.start`,
2933    /// `arc1.center`). Otherwise we reference the point directly.
2934    fn point_id_to_ast_reference(
2935        &self,
2936        point_id: ObjectId,
2937        new_ast: &mut ast::Node<ast::Program>,
2938    ) -> Result<ast::Expr, KclError> {
2939        let point_object = self
2940            .scene_graph
2941            .objects
2942            .get(point_id.0)
2943            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
2944        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
2945            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
2946        };
2947        let Segment::Point(point) = point_segment else {
2948            return Err(KclError::refactor(format!(
2949                "Only points are currently supported: {point_object:?}"
2950            )));
2951        };
2952
2953        if let Some(owner_id) = point.owner {
2954            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2955                KclError::refactor(format!(
2956                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
2957                ))
2958            })?;
2959            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
2960                return Err(KclError::refactor(format!(
2961                    "Owner of point is not a segment, but found {}",
2962                    owner_object.kind.human_friendly_kind_with_article()
2963                )));
2964            };
2965
2966            match owner_segment {
2967                Segment::Line(line) => {
2968                    let property = if line.start == point_id {
2969                        LINE_PROPERTY_START
2970                    } else if line.end == point_id {
2971                        LINE_PROPERTY_END
2972                    } else {
2973                        return Err(KclError::refactor(format!(
2974                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2975                        )));
2976                    };
2977                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
2978                }
2979                Segment::Arc(arc) => {
2980                    let property = if arc.start == point_id {
2981                        ARC_PROPERTY_START
2982                    } else if arc.end == point_id {
2983                        ARC_PROPERTY_END
2984                    } else if arc.center == point_id {
2985                        ARC_PROPERTY_CENTER
2986                    } else {
2987                        return Err(KclError::refactor(format!(
2988                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2989                        )));
2990                    };
2991                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
2992                }
2993                Segment::Circle(circle) => {
2994                    let property = if circle.start == point_id {
2995                        CIRCLE_PROPERTY_START
2996                    } else if circle.center == point_id {
2997                        CIRCLE_PROPERTY_CENTER
2998                    } else {
2999                        return Err(KclError::refactor(format!(
3000                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3001                        )));
3002                    };
3003                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3004                }
3005                _ => Err(KclError::refactor(format!(
3006                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3007                ))),
3008            }
3009        } else {
3010            // Standalone point.
3011            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3012        }
3013    }
3014
3015    fn coincident_segment_to_ast(
3016        &self,
3017        segment: &ConstraintSegment,
3018        new_ast: &mut ast::Node<ast::Program>,
3019    ) -> Result<ast::Expr, KclError> {
3020        match segment {
3021            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3022            ConstraintSegment::Segment(segment_id) => {
3023                let segment_object = self
3024                    .scene_graph
3025                    .objects
3026                    .get(segment_id.0)
3027                    .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3028                let ObjectKind::Segment { segment } = &segment_object.kind else {
3029                    return Err(KclError::refactor(format!(
3030                        "Object is not a segment, it is {}",
3031                        segment_object.kind.human_friendly_kind_with_article()
3032                    )));
3033                };
3034
3035                match segment {
3036                    Segment::Point(_) => self.point_id_to_ast_reference(*segment_id, new_ast),
3037                    Segment::Line(_) => {
3038                        get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None)
3039                    }
3040                    Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3041                    Segment::Circle(_) => {
3042                        get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None)
3043                    }
3044                }
3045            }
3046        }
3047    }
3048
3049    fn axis_constraint_segment_to_ast(
3050        &self,
3051        segment: &ConstraintSegment,
3052        new_ast: &mut ast::Node<ast::Program>,
3053    ) -> Result<ast::Expr, KclError> {
3054        match segment {
3055            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3056            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3057        }
3058    }
3059
3060    async fn add_coincident(
3061        &mut self,
3062        sketch: ObjectId,
3063        coincident: Coincident,
3064        new_ast: &mut ast::Node<ast::Program>,
3065    ) -> Result<AstNodeRef, KclError> {
3066        let sketch_id = sketch;
3067        let segment_asts = coincident
3068            .segments
3069            .iter()
3070            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3071            .collect::<Result<Vec<_>, _>>()?;
3072        if segment_asts.len() < 2 {
3073            return Err(KclError::refactor(format!(
3074                "Coincident constraint must have at least 2 inputs, got {}",
3075                segment_asts.len()
3076            )));
3077        }
3078
3079        // Create the coincident() call using shared helper.
3080        let coincident_ast = create_coincident_ast(segment_asts);
3081
3082        // Add the line to the AST of the sketch block.
3083        let (sketch_block_ref, _) = self.mutate_ast(
3084            new_ast,
3085            sketch_id,
3086            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3087        )?;
3088        Ok(sketch_block_ref)
3089    }
3090
3091    async fn add_distance(
3092        &mut self,
3093        sketch: ObjectId,
3094        distance: Distance,
3095        new_ast: &mut ast::Node<ast::Program>,
3096    ) -> Result<AstNodeRef, KclError> {
3097        let sketch_id = sketch;
3098        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3099            [pt0, pt1] => [
3100                self.coincident_segment_to_ast(pt0, new_ast)?,
3101                self.coincident_segment_to_ast(pt1, new_ast)?,
3102            ],
3103            _ => {
3104                return Err(KclError::refactor(format!(
3105                    "Distance constraint must have exactly 2 points, got {}",
3106                    distance.points.len()
3107                )));
3108            }
3109        };
3110
3111        let arguments = match &distance.label_position {
3112            Some(label_position) => vec![ast::LabeledArg {
3113                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3114                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3115            }],
3116            None => Default::default(),
3117        };
3118
3119        // Create the distance() call.
3120        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3121            callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3122            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3123                ast::ArrayExpression {
3124                    elements: vec![pt0_ast, pt1_ast],
3125                    digest: None,
3126                    non_code_meta: Default::default(),
3127                },
3128            )))),
3129            arguments,
3130            digest: None,
3131            non_code_meta: Default::default(),
3132        })));
3133        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3134            left: distance_call_ast,
3135            operator: ast::BinaryOperator::Eq,
3136            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3137                value: ast::LiteralValue::Number {
3138                    value: distance.distance.value,
3139                    suffix: distance.distance.units,
3140                },
3141                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3142                    KclError::refactor(format!(
3143                        "Could not format numeric suffix: {:?}",
3144                        distance.distance.units
3145                    ))
3146                })?,
3147                digest: None,
3148            }))),
3149            digest: None,
3150        })));
3151
3152        // Add the line to the AST of the sketch block.
3153        let (sketch_block_ref, _) = self.mutate_ast(
3154            new_ast,
3155            sketch_id,
3156            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3157        )?;
3158        Ok(sketch_block_ref)
3159    }
3160
3161    async fn add_angle(
3162        &mut self,
3163        sketch: ObjectId,
3164        angle: Angle,
3165        new_ast: &mut ast::Node<ast::Program>,
3166    ) -> Result<AstNodeRef, KclError> {
3167        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3168            return Err(KclError::refactor(format!(
3169                "Angle constraint must have exactly 2 lines, got {}",
3170                angle.lines.len()
3171            )));
3172        };
3173        let sketch_id = sketch;
3174
3175        // Map the runtime objects back to variable names.
3176        let line0_object = self
3177            .scene_graph
3178            .objects
3179            .get(l0_id.0)
3180            .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3181        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3182            return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3183        };
3184        let Segment::Line(_) = line0_segment else {
3185            return Err(KclError::refactor(format!(
3186                "Only lines can be constrained to meet at an angle: {line0_object:?}",
3187            )));
3188        };
3189        let l0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3190
3191        let line1_object = self
3192            .scene_graph
3193            .objects
3194            .get(l1_id.0)
3195            .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3196        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3197            return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3198        };
3199        let Segment::Line(_) = line1_segment else {
3200            return Err(KclError::refactor(format!(
3201                "Only lines can be constrained to meet at an angle: {line1_object:?}",
3202            )));
3203        };
3204        let l1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3205
3206        // Create the angle() call.
3207        let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3208            callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3209            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3210                ast::ArrayExpression {
3211                    elements: vec![l0_ast, l1_ast],
3212                    digest: None,
3213                    non_code_meta: Default::default(),
3214                },
3215            )))),
3216            arguments: Default::default(),
3217            digest: None,
3218            non_code_meta: Default::default(),
3219        })));
3220        let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3221            left: angle_call_ast,
3222            operator: ast::BinaryOperator::Eq,
3223            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3224                value: ast::LiteralValue::Number {
3225                    value: angle.angle.value,
3226                    suffix: angle.angle.units,
3227                },
3228                raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3229                    KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3230                })?,
3231                digest: None,
3232            }))),
3233            digest: None,
3234        })));
3235
3236        // Add the line to the AST of the sketch block.
3237        let (sketch_block_ref, _) = self.mutate_ast(
3238            new_ast,
3239            sketch_id,
3240            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3241        )?;
3242        Ok(sketch_block_ref)
3243    }
3244
3245    async fn add_tangent(
3246        &mut self,
3247        sketch: ObjectId,
3248        tangent: Tangent,
3249        new_ast: &mut ast::Node<ast::Program>,
3250    ) -> Result<AstNodeRef, KclError> {
3251        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3252            return Err(KclError::refactor(format!(
3253                "Tangent constraint must have exactly 2 segments, got {}",
3254                tangent.input.len()
3255            )));
3256        };
3257        let sketch_id = sketch;
3258
3259        let seg0_object = self
3260            .scene_graph
3261            .objects
3262            .get(seg0_id.0)
3263            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3264        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3265            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3266        };
3267        let seg0_ast = match seg0_segment {
3268            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, LINE_VARIABLE, None)?,
3269            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, ARC_VARIABLE, None)?,
3270            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg0_object.source, CIRCLE_VARIABLE, None)?,
3271            _ => {
3272                return Err(KclError::refactor(format!(
3273                    "Tangent supports only line/arc/circle segments, got: {seg0_segment:?}"
3274                )));
3275            }
3276        };
3277
3278        let seg1_object = self
3279            .scene_graph
3280            .objects
3281            .get(seg1_id.0)
3282            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3283        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3284            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3285        };
3286        let seg1_ast = match seg1_segment {
3287            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, LINE_VARIABLE, None)?,
3288            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, ARC_VARIABLE, None)?,
3289            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &seg1_object.source, CIRCLE_VARIABLE, None)?,
3290            _ => {
3291                return Err(KclError::refactor(format!(
3292                    "Tangent supports only line/arc/circle segments, got: {seg1_segment:?}"
3293                )));
3294            }
3295        };
3296
3297        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3298        let (sketch_block_ref, _) = self.mutate_ast(
3299            new_ast,
3300            sketch_id,
3301            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3302        )?;
3303        Ok(sketch_block_ref)
3304    }
3305
3306    async fn add_symmetric(
3307        &mut self,
3308        sketch: ObjectId,
3309        symmetric: Symmetric,
3310        new_ast: &mut ast::Node<ast::Program>,
3311    ) -> Result<AstNodeRef, KclError> {
3312        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3313            return Err(KclError::refactor(format!(
3314                "Symmetric constraint must have exactly 2 inputs, got {}",
3315                symmetric.input.len()
3316            )));
3317        };
3318        let sketch_id = sketch;
3319
3320        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3321        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3322        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3323
3324        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3325        let (sketch_block_ref, _) = self.mutate_ast(
3326            new_ast,
3327            sketch_id,
3328            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3329        )?;
3330        Ok(sketch_block_ref)
3331    }
3332
3333    async fn add_midpoint(
3334        &mut self,
3335        sketch: ObjectId,
3336        midpoint: Midpoint,
3337        new_ast: &mut ast::Node<ast::Program>,
3338    ) -> Result<AstNodeRef, KclError> {
3339        let sketch_id = sketch;
3340        let point_ast = self.point_id_to_ast_reference(midpoint.point, new_ast)?;
3341
3342        let segment_object = self
3343            .scene_graph
3344            .objects
3345            .get(midpoint.segment.0)
3346            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3347        let ObjectKind::Segment {
3348            segment: midpoint_segment,
3349        } = &segment_object.kind
3350        else {
3351            return Err(KclError::refactor(format!(
3352                "Object must be a segment, but it was {}",
3353                segment_object.kind.human_friendly_kind_with_article()
3354            )));
3355        };
3356        let segment_ast = match midpoint_segment {
3357            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "line", None)?,
3358            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3359            _ => {
3360                return Err(KclError::refactor(format!(
3361                    "Midpoint target must be a line or arc segment but it was {}",
3362                    midpoint_segment.human_friendly_kind_with_article()
3363                )));
3364            }
3365        };
3366
3367        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3368        let (sketch_block_ref, _) = self.mutate_ast(
3369            new_ast,
3370            sketch_id,
3371            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3372        )?;
3373        Ok(sketch_block_ref)
3374    }
3375
3376    async fn add_equal_radius(
3377        &mut self,
3378        sketch: ObjectId,
3379        equal_radius: EqualRadius,
3380        new_ast: &mut ast::Node<ast::Program>,
3381    ) -> Result<AstNodeRef, KclError> {
3382        if equal_radius.input.len() < 2 {
3383            return Err(KclError::refactor(format!(
3384                "equalRadius constraint must have at least 2 segments, got {}",
3385                equal_radius.input.len()
3386            )));
3387        }
3388
3389        let sketch_id = sketch;
3390        let input_asts = equal_radius
3391            .input
3392            .iter()
3393            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3394            .collect::<Result<Vec<_>, _>>()?;
3395
3396        let equal_radius_ast = create_equal_radius_ast(input_asts);
3397        let (sketch_block_ref, _) = self.mutate_ast(
3398            new_ast,
3399            sketch_id,
3400            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3401        )?;
3402        Ok(sketch_block_ref)
3403    }
3404
3405    async fn add_radius(
3406        &mut self,
3407        sketch: ObjectId,
3408        radius: Radius,
3409        new_ast: &mut ast::Node<ast::Program>,
3410    ) -> Result<AstNodeRef, KclError> {
3411        let params = ArcSizeConstraintParams {
3412            points: vec![radius.arc],
3413            function_name: RADIUS_FN,
3414            value: radius.radius.value,
3415            units: radius.radius.units,
3416            label_position: radius.label_position,
3417            constraint_type_name: "Radius",
3418        };
3419        self.add_arc_size_constraint(sketch, params, new_ast).await
3420    }
3421
3422    async fn add_diameter(
3423        &mut self,
3424        sketch: ObjectId,
3425        diameter: Diameter,
3426        new_ast: &mut ast::Node<ast::Program>,
3427    ) -> Result<AstNodeRef, KclError> {
3428        let params = ArcSizeConstraintParams {
3429            points: vec![diameter.arc],
3430            function_name: DIAMETER_FN,
3431            value: diameter.diameter.value,
3432            units: diameter.diameter.units,
3433            label_position: diameter.label_position,
3434            constraint_type_name: "Diameter",
3435        };
3436        self.add_arc_size_constraint(sketch, params, new_ast).await
3437    }
3438
3439    async fn add_fixed_constraints(
3440        &mut self,
3441        sketch: ObjectId,
3442        points: Vec<FixedPoint>,
3443        new_ast: &mut ast::Node<ast::Program>,
3444    ) -> Result<AstNodeRef, KclError> {
3445        let mut sketch_block_ref = None;
3446
3447        for fixed_point in points {
3448            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
3449            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
3450                .map_err(|err| KclError::refactor(err.to_string()))?;
3451
3452            let (sketch_ref, _) = self.mutate_ast(
3453                new_ast,
3454                sketch,
3455                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
3456            )?;
3457            sketch_block_ref = Some(sketch_ref);
3458        }
3459
3460        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
3461    }
3462
3463    async fn add_arc_size_constraint(
3464        &mut self,
3465        sketch: ObjectId,
3466        params: ArcSizeConstraintParams,
3467        new_ast: &mut ast::Node<ast::Program>,
3468    ) -> Result<AstNodeRef, KclError> {
3469        let sketch_id = sketch;
3470
3471        // Constraint must have exactly 1 argument (arc segment)
3472        if params.points.len() != 1 {
3473            return Err(KclError::refactor(format!(
3474                "{} constraint must have exactly 1 argument (an arc segment), got {}",
3475                params.constraint_type_name,
3476                params.points.len()
3477            )));
3478        }
3479
3480        let arc_id = params.points[0];
3481        let arc_object = self
3482            .scene_graph
3483            .objects
3484            .get(arc_id.0)
3485            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
3486        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
3487            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
3488        };
3489        let ref_type = match arc_segment {
3490            Segment::Arc(_) => ARC_VARIABLE,
3491            Segment::Circle(_) => CIRCLE_VARIABLE,
3492            _ => {
3493                return Err(KclError::refactor(format!(
3494                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
3495                    params.constraint_type_name
3496                )));
3497            }
3498        };
3499        // Reference the arc/circle segment directly
3500        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
3501        let arguments = match &params.label_position {
3502            Some(label_position) => vec![ast::LabeledArg {
3503                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3504                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3505            }],
3506            None => Default::default(),
3507        };
3508
3509        // Create the function call.
3510        let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3511            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
3512            unlabeled: Some(arc_ast),
3513            arguments,
3514            digest: None,
3515            non_code_meta: Default::default(),
3516        })));
3517        let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3518            left: call_ast,
3519            operator: ast::BinaryOperator::Eq,
3520            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3521                value: ast::LiteralValue::Number {
3522                    value: params.value,
3523                    suffix: params.units,
3524                },
3525                raw: format_number_literal(params.value, params.units, None)
3526                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
3527                digest: None,
3528            }))),
3529            digest: None,
3530        })));
3531
3532        // Add the line to the AST of the sketch block.
3533        let (sketch_block_ref, _) = self.mutate_ast(
3534            new_ast,
3535            sketch_id,
3536            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
3537        )?;
3538        Ok(sketch_block_ref)
3539    }
3540
3541    async fn add_horizontal_distance(
3542        &mut self,
3543        sketch: ObjectId,
3544        distance: Distance,
3545        new_ast: &mut ast::Node<ast::Program>,
3546    ) -> Result<AstNodeRef, KclError> {
3547        let sketch_id = sketch;
3548        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3549            [pt0, pt1] => [
3550                self.coincident_segment_to_ast(pt0, new_ast)?,
3551                self.coincident_segment_to_ast(pt1, new_ast)?,
3552            ],
3553            _ => {
3554                return Err(KclError::refactor(format!(
3555                    "Horizontal distance constraint must have exactly 2 points, got {}",
3556                    distance.points.len()
3557                )));
3558            }
3559        };
3560
3561        let arguments = match &distance.label_position {
3562            Some(label_position) => vec![ast::LabeledArg {
3563                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3564                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3565            }],
3566            None => Default::default(),
3567        };
3568
3569        // Create the horizontalDistance() call.
3570        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3571            callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
3572            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3573                ast::ArrayExpression {
3574                    elements: vec![pt0_ast, pt1_ast],
3575                    digest: None,
3576                    non_code_meta: Default::default(),
3577                },
3578            )))),
3579            arguments,
3580            digest: None,
3581            non_code_meta: Default::default(),
3582        })));
3583        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3584            left: distance_call_ast,
3585            operator: ast::BinaryOperator::Eq,
3586            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3587                value: ast::LiteralValue::Number {
3588                    value: distance.distance.value,
3589                    suffix: distance.distance.units,
3590                },
3591                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3592                    KclError::refactor(format!(
3593                        "Could not format numeric suffix: {:?}",
3594                        distance.distance.units
3595                    ))
3596                })?,
3597                digest: None,
3598            }))),
3599            digest: None,
3600        })));
3601
3602        // Add the line to the AST of the sketch block.
3603        let (sketch_block_ref, _) = self.mutate_ast(
3604            new_ast,
3605            sketch_id,
3606            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3607        )?;
3608        Ok(sketch_block_ref)
3609    }
3610
3611    async fn add_vertical_distance(
3612        &mut self,
3613        sketch: ObjectId,
3614        distance: Distance,
3615        new_ast: &mut ast::Node<ast::Program>,
3616    ) -> Result<AstNodeRef, KclError> {
3617        let sketch_id = sketch;
3618        let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3619            [pt0, pt1] => [
3620                self.coincident_segment_to_ast(pt0, new_ast)?,
3621                self.coincident_segment_to_ast(pt1, new_ast)?,
3622            ],
3623            _ => {
3624                return Err(KclError::refactor(format!(
3625                    "Vertical distance constraint must have exactly 2 points, got {}",
3626                    distance.points.len()
3627                )));
3628            }
3629        };
3630
3631        let arguments = match &distance.label_position {
3632            Some(label_position) => vec![ast::LabeledArg {
3633                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3634                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3635            }],
3636            None => Default::default(),
3637        };
3638
3639        // Create the verticalDistance() call.
3640        let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3641            callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
3642            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3643                ast::ArrayExpression {
3644                    elements: vec![pt0_ast, pt1_ast],
3645                    digest: None,
3646                    non_code_meta: Default::default(),
3647                },
3648            )))),
3649            arguments,
3650            digest: None,
3651            non_code_meta: Default::default(),
3652        })));
3653        let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3654            left: distance_call_ast,
3655            operator: ast::BinaryOperator::Eq,
3656            right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3657                value: ast::LiteralValue::Number {
3658                    value: distance.distance.value,
3659                    suffix: distance.distance.units,
3660                },
3661                raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3662                    KclError::refactor(format!(
3663                        "Could not format numeric suffix: {:?}",
3664                        distance.distance.units
3665                    ))
3666                })?,
3667                digest: None,
3668            }))),
3669            digest: None,
3670        })));
3671
3672        // Add the line to the AST of the sketch block.
3673        let (sketch_block_ref, _) = self.mutate_ast(
3674            new_ast,
3675            sketch_id,
3676            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3677        )?;
3678        Ok(sketch_block_ref)
3679    }
3680
3681    async fn add_horizontal(
3682        &mut self,
3683        sketch: ObjectId,
3684        horizontal: Horizontal,
3685        new_ast: &mut ast::Node<ast::Program>,
3686    ) -> Result<AstNodeRef, KclError> {
3687        let sketch_id = sketch;
3688
3689        // Map the runtime objects back to variable names.
3690        let first_arg_ast = match horizontal {
3691            Horizontal::Line { line } => {
3692                let line_object = self
3693                    .scene_graph
3694                    .objects
3695                    .get(line.0)
3696                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
3697                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3698                    let kind = line_object.kind.human_friendly_kind_with_article();
3699                    return Err(KclError::refactor(format!(
3700                        "This constraint only works on Segments, but you selected {kind}"
3701                    )));
3702                };
3703                let Segment::Line(_) = line_segment else {
3704                    return Err(KclError::refactor(format!(
3705                        "Only lines can be made horizontal, but you selected {}",
3706                        line_segment.human_friendly_kind_with_article(),
3707                    )));
3708                };
3709                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
3710            }
3711            Horizontal::Points { points } => {
3712                let point_asts = points
3713                    .iter()
3714                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3715                    .collect::<Result<Vec<_>, _>>()?;
3716                ast::ArrayExpression::new(point_asts).into()
3717            }
3718        };
3719
3720        // Create the horizontal() call using shared helper.
3721        let horizontal_ast = create_horizontal_ast(first_arg_ast);
3722
3723        // Add the line to the AST of the sketch block.
3724        let (sketch_block_ref, _) = self.mutate_ast(
3725            new_ast,
3726            sketch_id,
3727            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
3728        )?;
3729        Ok(sketch_block_ref)
3730    }
3731
3732    async fn add_lines_equal_length(
3733        &mut self,
3734        sketch: ObjectId,
3735        lines_equal_length: LinesEqualLength,
3736        new_ast: &mut ast::Node<ast::Program>,
3737    ) -> Result<AstNodeRef, KclError> {
3738        if lines_equal_length.lines.len() < 2 {
3739            return Err(KclError::refactor(format!(
3740                "Lines equal length constraint must have at least 2 lines, got {}",
3741                lines_equal_length.lines.len()
3742            )));
3743        };
3744
3745        let sketch_id = sketch;
3746
3747        // Map the runtime objects back to variable names.
3748        let line_asts = lines_equal_length
3749            .lines
3750            .iter()
3751            .map(|line_id| {
3752                let line_object = self
3753                    .scene_graph
3754                    .objects
3755                    .get(line_id.0)
3756                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3757                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3758                    let kind = line_object.kind.human_friendly_kind_with_article();
3759                    return Err(KclError::refactor(format!(
3760                        "This constraint only works on Segments, but you selected {kind}"
3761                    )));
3762                };
3763                let Segment::Line(_) = line_segment else {
3764                    let kind = line_segment.human_friendly_kind_with_article();
3765                    return Err(KclError::refactor(format!(
3766                        "Only lines can be made equal length, but you selected {kind}"
3767                    )));
3768                };
3769
3770                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3771            })
3772            .collect::<Result<Vec<_>, _>>()?;
3773
3774        // Create the equalLength() call using shared helper.
3775        let equal_length_ast = create_equal_length_ast(line_asts);
3776
3777        // Add the constraint to the AST of the sketch block.
3778        let (sketch_block_ref, _) = self.mutate_ast(
3779            new_ast,
3780            sketch_id,
3781            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
3782        )?;
3783        Ok(sketch_block_ref)
3784    }
3785
3786    fn equal_radius_segment_id_to_ast_reference(
3787        &mut self,
3788        segment_id: ObjectId,
3789        new_ast: &mut ast::Node<ast::Program>,
3790    ) -> Result<ast::Expr, KclError> {
3791        let segment_object = self
3792            .scene_graph
3793            .objects
3794            .get(segment_id.0)
3795            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3796        let ObjectKind::Segment { segment } = &segment_object.kind else {
3797            return Err(KclError::refactor(format!(
3798                "Object is not a segment, it was {}",
3799                segment_object.kind.human_friendly_kind_with_article()
3800            )));
3801        };
3802
3803        let ref_type = match segment {
3804            Segment::Arc(_) => ARC_VARIABLE,
3805            Segment::Circle(_) => CIRCLE_VARIABLE,
3806            _ => {
3807                return Err(KclError::refactor(format!(
3808                    "equalRadius supports only arc/circle segments, got {}",
3809                    segment.human_friendly_kind_with_article()
3810                )));
3811            }
3812        };
3813
3814        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
3815    }
3816
3817    fn symmetric_input_id_to_ast_reference(
3818        &mut self,
3819        segment_id: ObjectId,
3820        new_ast: &mut ast::Node<ast::Program>,
3821    ) -> Result<ast::Expr, KclError> {
3822        let segment_object = self
3823            .scene_graph
3824            .objects
3825            .get(segment_id.0)
3826            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
3827        let ObjectKind::Segment { segment } = &segment_object.kind else {
3828            return Err(KclError::refactor(format!(
3829                "Object is not a segment, it was {}",
3830                segment_object.kind.human_friendly_kind_with_article()
3831            )));
3832        };
3833
3834        match segment {
3835            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3836            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3837            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
3838            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3839        }
3840    }
3841
3842    fn symmetric_axis_id_to_ast_reference(
3843        &mut self,
3844        segment_id: ObjectId,
3845        new_ast: &mut ast::Node<ast::Program>,
3846    ) -> Result<ast::Expr, KclError> {
3847        let segment_object = self
3848            .scene_graph
3849            .objects
3850            .get(segment_id.0)
3851            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
3852        let ObjectKind::Segment { segment } = &segment_object.kind else {
3853            return Err(KclError::refactor(format!(
3854                "Object is not a segment, it was {}",
3855                segment_object.kind.human_friendly_kind_with_article()
3856            )));
3857        };
3858        match segment {
3859            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
3860            _ => Err(KclError::refactor(format!(
3861                "Symmetric axis must be a line, got {}",
3862                segment.human_friendly_kind_with_article()
3863            ))),
3864        }
3865    }
3866
3867    async fn add_parallel(
3868        &mut self,
3869        sketch: ObjectId,
3870        parallel: Parallel,
3871        new_ast: &mut ast::Node<ast::Program>,
3872    ) -> Result<AstNodeRef, KclError> {
3873        if parallel.lines.len() < 2 {
3874            return Err(KclError::refactor(format!(
3875                "Parallel constraint must have at least 2 lines, got {}",
3876                parallel.lines.len()
3877            )));
3878        };
3879
3880        let sketch_id = sketch;
3881
3882        let line_asts = parallel
3883            .lines
3884            .iter()
3885            .map(|line_id| {
3886                let line_object = self
3887                    .scene_graph
3888                    .objects
3889                    .get(line_id.0)
3890                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3891                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3892                    let kind = line_object.kind.human_friendly_kind_with_article();
3893                    return Err(KclError::refactor(format!(
3894                        "This constraint only works on Segments, but you selected {kind}"
3895                    )));
3896                };
3897                let Segment::Line(_) = line_segment else {
3898                    let kind = line_segment.human_friendly_kind_with_article();
3899                    return Err(KclError::refactor(format!(
3900                        "Only lines can be made parallel, but you selected {kind}"
3901                    )));
3902                };
3903
3904                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3905            })
3906            .collect::<Result<Vec<_>, _>>()?;
3907
3908        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3909            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
3910            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3911                ast::ArrayExpression {
3912                    elements: line_asts,
3913                    digest: None,
3914                    non_code_meta: Default::default(),
3915                },
3916            )))),
3917            arguments: Default::default(),
3918            digest: None,
3919            non_code_meta: Default::default(),
3920        })));
3921
3922        let (sketch_block_ref, _) = self.mutate_ast(
3923            new_ast,
3924            sketch_id,
3925            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
3926        )?;
3927        Ok(sketch_block_ref)
3928    }
3929
3930    async fn add_perpendicular(
3931        &mut self,
3932        sketch: ObjectId,
3933        perpendicular: Perpendicular,
3934        new_ast: &mut ast::Node<ast::Program>,
3935    ) -> Result<AstNodeRef, KclError> {
3936        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
3937            .await
3938    }
3939
3940    async fn add_lines_at_angle_constraint(
3941        &mut self,
3942        sketch: ObjectId,
3943        angle_kind: LinesAtAngleKind,
3944        lines: Vec<ObjectId>,
3945        new_ast: &mut ast::Node<ast::Program>,
3946    ) -> Result<AstNodeRef, KclError> {
3947        let &[line0_id, line1_id] = lines.as_slice() else {
3948            return Err(KclError::refactor(format!(
3949                "{} constraint must have exactly 2 lines, got {}",
3950                angle_kind.to_function_name(),
3951                lines.len()
3952            )));
3953        };
3954
3955        let sketch_id = sketch;
3956
3957        // Map the runtime objects back to variable names.
3958        let line0_object = self
3959            .scene_graph
3960            .objects
3961            .get(line0_id.0)
3962            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
3963        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3964            let kind = line0_object.kind.human_friendly_kind_with_article();
3965            return Err(KclError::refactor(format!(
3966                "This constraint only works on Segments, but you selected {kind}"
3967            )));
3968        };
3969        let Segment::Line(_) = line0_segment else {
3970            return Err(KclError::refactor(format!(
3971                "Only lines can be made {}, but you selected {}",
3972                angle_kind.to_function_name(),
3973                line0_segment.human_friendly_kind_with_article(),
3974            )));
3975        };
3976        let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), LINE_VARIABLE, None)?;
3977
3978        let line1_object = self
3979            .scene_graph
3980            .objects
3981            .get(line1_id.0)
3982            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
3983        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3984            let kind = line1_object.kind.human_friendly_kind_with_article();
3985            return Err(KclError::refactor(format!(
3986                "This constraint only works on Segments, but you selected {kind}"
3987            )));
3988        };
3989        let Segment::Line(_) = line1_segment else {
3990            return Err(KclError::refactor(format!(
3991                "Only lines can be made {}, but you selected {}",
3992                angle_kind.to_function_name(),
3993                line1_segment.human_friendly_kind_with_article(),
3994            )));
3995        };
3996        let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), LINE_VARIABLE, None)?;
3997
3998        // Create the parallel() or perpendicular() call.
3999        let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4000            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4001            unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4002                ast::ArrayExpression {
4003                    elements: vec![line0_ast, line1_ast],
4004                    digest: None,
4005                    non_code_meta: Default::default(),
4006                },
4007            )))),
4008            arguments: Default::default(),
4009            digest: None,
4010            non_code_meta: Default::default(),
4011        })));
4012
4013        // Add the constraint to the AST of the sketch block.
4014        let (sketch_block_ref, _) = self.mutate_ast(
4015            new_ast,
4016            sketch_id,
4017            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4018        )?;
4019        Ok(sketch_block_ref)
4020    }
4021
4022    async fn add_vertical(
4023        &mut self,
4024        sketch: ObjectId,
4025        vertical: Vertical,
4026        new_ast: &mut ast::Node<ast::Program>,
4027    ) -> Result<AstNodeRef, KclError> {
4028        let sketch_id = sketch;
4029
4030        let first_arg_ast = match vertical {
4031            Vertical::Line { line } => {
4032                // Map the runtime objects back to variable names.
4033                let line_object = self
4034                    .scene_graph
4035                    .objects
4036                    .get(line.0)
4037                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4038                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4039                    let kind = line_object.kind.human_friendly_kind_with_article();
4040                    return Err(KclError::refactor(format!(
4041                        "This constraint only works on Segments, but you selected {kind}"
4042                    )));
4043                };
4044                let Segment::Line(_) = line_segment else {
4045                    return Err(KclError::refactor(format!(
4046                        "Only lines can be made vertical, but you selected {}",
4047                        line_segment.human_friendly_kind_with_article()
4048                    )));
4049                };
4050                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)?
4051            }
4052            Vertical::Points { points } => {
4053                let point_asts = points
4054                    .iter()
4055                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4056                    .collect::<Result<Vec<_>, _>>()?;
4057                ast::ArrayExpression::new(point_asts).into()
4058            }
4059        };
4060
4061        // Create the vertical() call using shared helper.
4062        let vertical_ast = create_vertical_ast(first_arg_ast);
4063
4064        // Add the line to the AST of the sketch block.
4065        let (sketch_block_ref, _) = self.mutate_ast(
4066            new_ast,
4067            sketch_id,
4068            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4069        )?;
4070        Ok(sketch_block_ref)
4071    }
4072
4073    async fn execute_after_add_constraint(
4074        &mut self,
4075        ctx: &ExecutorContext,
4076        sketch_id: ObjectId,
4077        #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_ref: AstNodeRef,
4078        new_ast: &mut ast::Node<ast::Program>,
4079    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4080        // Convert to string source to create real source ranges.
4081        let new_source = source_from_ast(new_ast);
4082        // Parse the new KCL source.
4083        let (new_program, errors) = Program::parse(&new_source)
4084            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4085        if !errors.is_empty() {
4086            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4087                "Error parsing KCL source after adding constraint: {errors:?}"
4088            ))));
4089        }
4090        let Some(new_program) = new_program else {
4091            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4092                "No AST produced after adding constraint".to_string(),
4093            )));
4094        };
4095        #[cfg(feature = "artifact-graph")]
4096        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4097            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4098                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4099            )))
4100        })?;
4101
4102        // Truncate after the sketch block for mock execution.
4103        // Use a clone so we don't mutate new_program yet
4104        let mut truncated_program = new_program.clone();
4105        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4106            .map_err(KclErrorWithOutputs::no_outputs)?;
4107
4108        // Execute - if this fails, we haven't modified self yet, so state is safe
4109        let outcome = ctx
4110            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4111            .await?;
4112
4113        #[cfg(not(feature = "artifact-graph"))]
4114        let new_object_ids = Vec::new();
4115        #[cfg(feature = "artifact-graph")]
4116        let new_object_ids = {
4117            // Extract the constraint ID from the execution outcome using source_range_to_object
4118            let constraint_id = outcome
4119                .source_range_to_object
4120                .get(&constraint_node_ref.range)
4121                .copied()
4122                .ok_or_else(|| {
4123                    KclErrorWithOutputs::from_error_outcome(
4124                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4125                        outcome.clone(),
4126                    )
4127                })?;
4128            vec![constraint_id]
4129        };
4130
4131        // Only now, after all operations succeeded, update self.program
4132        // This ensures state is only modified if everything succeeds
4133        self.program = new_program;
4134
4135        // Uses MockConfig::default() which has freedom_analysis: true
4136        let outcome = self.update_state_after_exec(outcome, true);
4137
4138        let src_delta = SourceDelta { text: new_source };
4139        let scene_graph_delta = SceneGraphDelta {
4140            new_graph: self.scene_graph.clone(),
4141            invalidates_ids: false,
4142            new_objects: new_object_ids,
4143            exec_outcome: outcome,
4144        };
4145        Ok((src_delta, scene_graph_delta))
4146    }
4147
4148    // Find constraints that reference the given segments.
4149    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4150        if segment_ids_set.contains(&segment_id) {
4151            return true;
4152        }
4153
4154        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4155            return false;
4156        };
4157        let ObjectKind::Segment { segment } = &segment_object.kind else {
4158            return false;
4159        };
4160        let Segment::Point(point) = segment else {
4161            return false;
4162        };
4163
4164        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4165    }
4166
4167    fn remaining_constraint_segments(
4168        &self,
4169        segments: &[ConstraintSegment],
4170        segment_ids_set: &AhashIndexSet<ObjectId>,
4171    ) -> Vec<ConstraintSegment> {
4172        segments
4173            .iter()
4174            .copied()
4175            .filter(|segment| match segment {
4176                ConstraintSegment::Origin(_) => true,
4177                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4178            })
4179            .collect()
4180    }
4181
4182    fn find_referenced_constraints(
4183        &self,
4184        sketch_id: ObjectId,
4185        segment_ids_set: &AhashIndexSet<ObjectId>,
4186    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4187        // Look up the sketch.
4188        let sketch_object = self
4189            .scene_graph
4190            .objects
4191            .get(sketch_id.0)
4192            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4193        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4194            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4195        };
4196        let mut constraint_ids_set = AhashIndexSet::default();
4197        for constraint_id in &sketch.constraints {
4198            let constraint_object = self
4199                .scene_graph
4200                .objects
4201                .get(constraint_id.0)
4202                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4203            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4204                return Err(KclError::refactor(format!(
4205                    "Object is not a constraint, it is {}",
4206                    constraint_object.kind.human_friendly_kind_with_article()
4207                )));
4208            };
4209            let depends_on_segment = match constraint {
4210                Constraint::Coincident(c) => c
4211                    .segment_ids()
4212                    .any(|seg_id| self.segment_will_be_deleted(seg_id, segment_ids_set)),
4213                Constraint::Distance(d) => d
4214                    .point_ids()
4215                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4216                Constraint::Fixed(fixed) => fixed
4217                    .points
4218                    .iter()
4219                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4220                Constraint::Radius(r) => self.segment_will_be_deleted(r.arc, segment_ids_set),
4221                Constraint::Diameter(d) => self.segment_will_be_deleted(d.arc, segment_ids_set),
4222                Constraint::EqualRadius(equal_radius) => equal_radius
4223                    .input
4224                    .iter()
4225                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4226                Constraint::HorizontalDistance(d) => d
4227                    .point_ids()
4228                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4229                Constraint::VerticalDistance(d) => d
4230                    .point_ids()
4231                    .any(|pt_id| self.segment_will_be_deleted(pt_id, segment_ids_set)),
4232                Constraint::Horizontal(h) => match h {
4233                    Horizontal::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4234                    Horizontal::Points { points } => points.iter().any(|point| match point {
4235                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4236                        ConstraintSegment::Origin(_) => false,
4237                    }),
4238                },
4239                Constraint::Vertical(v) => match v {
4240                    Vertical::Line { line } => self.segment_will_be_deleted(*line, segment_ids_set),
4241                    Vertical::Points { points } => points.iter().any(|point| match point {
4242                        ConstraintSegment::Segment(point) => self.segment_will_be_deleted(*point, segment_ids_set),
4243                        ConstraintSegment::Origin(_) => false,
4244                    }),
4245                },
4246                Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
4247                    .lines
4248                    .iter()
4249                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4250                Constraint::Midpoint(midpoint) => {
4251                    self.segment_will_be_deleted(midpoint.segment, segment_ids_set)
4252                        || self.segment_will_be_deleted(midpoint.point, segment_ids_set)
4253                }
4254                Constraint::Parallel(parallel) => parallel
4255                    .lines
4256                    .iter()
4257                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4258                Constraint::Perpendicular(perpendicular) => perpendicular
4259                    .lines
4260                    .iter()
4261                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4262                Constraint::Angle(angle) => angle
4263                    .lines
4264                    .iter()
4265                    .any(|line_id| self.segment_will_be_deleted(*line_id, segment_ids_set)),
4266                Constraint::Symmetric(symmetric) => {
4267                    self.segment_will_be_deleted(symmetric.axis, segment_ids_set)
4268                        || symmetric
4269                            .input
4270                            .iter()
4271                            .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set))
4272                }
4273                Constraint::Tangent(tangent) => tangent
4274                    .input
4275                    .iter()
4276                    .any(|seg_id| self.segment_will_be_deleted(*seg_id, segment_ids_set)),
4277            };
4278            if depends_on_segment {
4279                constraint_ids_set.insert(*constraint_id);
4280            }
4281        }
4282        Ok(constraint_ids_set)
4283    }
4284
4285    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4286        #[cfg(not(feature = "artifact-graph"))]
4287        {
4288            let _ = freedom_analysis_ran; // Only used when artifact-graph feature is enabled
4289            outcome
4290        }
4291        #[cfg(feature = "artifact-graph")]
4292        {
4293            let mut outcome = outcome;
4294            let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4295
4296            if freedom_analysis_ran {
4297                // When freedom analysis ran, replace the cache entirely with new values
4298                // Don't merge with old values since IDs might have changed
4299                self.point_freedom_cache.clear();
4300                for new_obj in &new_objects {
4301                    if let ObjectKind::Segment {
4302                        segment: crate::front::Segment::Point(point),
4303                    } = &new_obj.kind
4304                    {
4305                        self.point_freedom_cache.insert(new_obj.id, point.freedom);
4306                    }
4307                }
4308                add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4309                // Objects are already correct from the analysis, just use them as-is
4310                self.scene_graph.objects = new_objects;
4311            } else {
4312                // When freedom analysis didn't run, preserve old values and merge
4313                // Before replacing objects, extract and store freedom values from old objects
4314                for old_obj in &self.scene_graph.objects {
4315                    if let ObjectKind::Segment {
4316                        segment: crate::front::Segment::Point(point),
4317                    } = &old_obj.kind
4318                    {
4319                        self.point_freedom_cache.insert(old_obj.id, point.freedom);
4320                    }
4321                }
4322
4323                // Update objects, preserving stored freedom values when new is Free (might be default)
4324                let mut updated_objects = Vec::with_capacity(new_objects.len());
4325                for new_obj in new_objects {
4326                    let mut obj = new_obj;
4327                    if let ObjectKind::Segment {
4328                        segment: crate::front::Segment::Point(point),
4329                    } = &mut obj.kind
4330                    {
4331                        let new_freedom = point.freedom;
4332                        // When freedom_analysis=false, new values are defaults (Free).
4333                        // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4334                        // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4335                        // Never preserve Conflict from cache - conflicts are transient and should only be set
4336                        // when there are actually unsatisfied constraints.
4337                        match new_freedom {
4338                            Freedom::Free => {
4339                                match self.point_freedom_cache.get(&obj.id).copied() {
4340                                    Some(Freedom::Conflict) => {
4341                                        // Don't preserve Conflict - conflicts are transient
4342                                        // Keep it as Free
4343                                    }
4344                                    Some(Freedom::Fixed) => {
4345                                        // Preserve Fixed cached value
4346                                        point.freedom = Freedom::Fixed;
4347                                    }
4348                                    Some(Freedom::Free) => {
4349                                        // If stored is also Free, keep Free (no change needed)
4350                                    }
4351                                    None => {
4352                                        // If no cached value, keep Free (default)
4353                                    }
4354                                }
4355                            }
4356                            Freedom::Fixed => {
4357                                // Use new value (already set)
4358                            }
4359                            Freedom::Conflict => {
4360                                // Use new value (already set)
4361                            }
4362                        }
4363                        // Store the new freedom value (even if it's Free, so we know it was set)
4364                        self.point_freedom_cache.insert(obj.id, point.freedom);
4365                    }
4366                    updated_objects.push(obj);
4367                }
4368
4369                add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4370                self.scene_graph.objects = updated_objects;
4371            }
4372            outcome
4373        }
4374    }
4375
4376    fn mutate_ast(
4377        &mut self,
4378        ast: &mut ast::Node<ast::Program>,
4379        object_id: ObjectId,
4380        command: AstMutateCommand,
4381    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4382        let sketch_object = self
4383            .scene_graph
4384            .objects
4385            .get(object_id.0)
4386            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4387        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4388    }
4389}
4390
4391fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
4392    // Look up existing sketch.
4393    let sketch_object = scene_graph
4394        .objects
4395        .get(sketch_id.0)
4396        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4397    let ObjectKind::Sketch(_) = &sketch_object.kind else {
4398        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4399    };
4400    expect_single_node_ref(sketch_object)
4401}
4402
4403fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
4404    match &object.source {
4405        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
4406            range: *range,
4407            node_path: node_path.clone(),
4408        }),
4409        SourceRef::BackTrace { ranges } => {
4410            let [range] = ranges.as_slice() else {
4411                return Err(KclError::refactor(format!(
4412                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
4413                    ranges.len()
4414                )));
4415            };
4416            Ok(AstNodeRef {
4417                range: range.0,
4418                node_path: range.1.clone(),
4419            })
4420        }
4421    }
4422}
4423
4424/// This is a deprecated fall-back implementation. Prefer
4425/// [`only_sketch_block()`] to avoid reliance on source ranges.
4426fn only_sketch_block_from_range(
4427    ast: &mut ast::Node<ast::Program>,
4428    sketch_block_range: SourceRange,
4429    edit_kind: ChangeKind,
4430) -> Result<(), KclError> {
4431    let r1 = sketch_block_range;
4432    let matches_range = |r2: SourceRange| -> bool {
4433        // We may have added items to the sketch block, so the end may not be an
4434        // exact match.
4435        match edit_kind {
4436            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
4437            // For edit, we don't know whether it grew or shrank.
4438            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
4439            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
4440            // No edit should be an exact match.
4441            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
4442        }
4443    };
4444    let mut found = false;
4445    for item in ast.body.iter_mut() {
4446        match item {
4447            ast::BodyItem::ImportStatement(_) => {}
4448            ast::BodyItem::ExpressionStatement(node) => {
4449                if matches_range(SourceRange::from(&*node))
4450                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4451                {
4452                    sketch_block.is_being_edited = true;
4453                    found = true;
4454                    break;
4455                }
4456            }
4457            ast::BodyItem::VariableDeclaration(node) => {
4458                if matches_range(SourceRange::from(&node.declaration.init))
4459                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4460                {
4461                    sketch_block.is_being_edited = true;
4462                    found = true;
4463                    break;
4464                }
4465            }
4466            ast::BodyItem::TypeDeclaration(_) => {}
4467            ast::BodyItem::ReturnStatement(node) => {
4468                if matches_range(SourceRange::from(&node.argument))
4469                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4470                {
4471                    sketch_block.is_being_edited = true;
4472                    found = true;
4473                    break;
4474                }
4475            }
4476        }
4477    }
4478    if !found {
4479        return Err(KclError::refactor(format!(
4480            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
4481        )));
4482    }
4483
4484    Ok(())
4485}
4486
4487fn only_sketch_block(
4488    ast: &mut ast::Node<ast::Program>,
4489    sketch_block_ref: &AstNodeRef,
4490    edit_kind: ChangeKind,
4491) -> Result<(), KclError> {
4492    let Some(target_node_path) = &sketch_block_ref.node_path else {
4493        #[cfg(target_arch = "wasm32")]
4494        web_sys::console::warn_1(
4495            &format!(
4496                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
4497                &sketch_block_ref
4498            )
4499            .into(),
4500        );
4501        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
4502    };
4503    let mut found = false;
4504    for item in ast.body.iter_mut() {
4505        match item {
4506            ast::BodyItem::ImportStatement(_) => {}
4507            ast::BodyItem::ExpressionStatement(node) => {
4508                // Check the statement.
4509                if let Some(node_path) = &node.node_path
4510                    && node_path == target_node_path
4511                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4512                {
4513                    sketch_block.is_being_edited = true;
4514                    found = true;
4515                    break;
4516                }
4517                // Check the expression.
4518                if let Some(node_path) = node.expression.node_path()
4519                    && node_path == target_node_path
4520                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
4521                {
4522                    sketch_block.is_being_edited = true;
4523                    found = true;
4524                    break;
4525                }
4526            }
4527            ast::BodyItem::VariableDeclaration(node) => {
4528                if let Some(node_path) = node.declaration.init.node_path()
4529                    && node_path == target_node_path
4530                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
4531                {
4532                    sketch_block.is_being_edited = true;
4533                    found = true;
4534                    break;
4535                }
4536            }
4537            ast::BodyItem::TypeDeclaration(_) => {}
4538            ast::BodyItem::ReturnStatement(node) => {
4539                if let Some(node_path) = node.argument.node_path()
4540                    && node_path == target_node_path
4541                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
4542                {
4543                    sketch_block.is_being_edited = true;
4544                    found = true;
4545                    break;
4546                }
4547            }
4548        }
4549    }
4550    if !found {
4551        return Err(KclError::refactor(format!(
4552            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
4553        )));
4554    }
4555
4556    Ok(())
4557}
4558
4559fn sketch_on_ast_expr(
4560    ast: &mut ast::Node<ast::Program>,
4561    scene_graph: &SceneGraph,
4562    on: &Plane,
4563) -> Result<ast::Expr, KclError> {
4564    match on {
4565        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
4566        Plane::Object(object_id) => {
4567            let on_object = scene_graph
4568                .objects
4569                .get(object_id.0)
4570                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
4571            #[cfg(feature = "artifact-graph")]
4572            {
4573                if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
4574                    return Ok(face_expr);
4575                }
4576            }
4577            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
4578        }
4579    }
4580}
4581
4582#[cfg(feature = "artifact-graph")]
4583fn sketch_face_of_scene_object_ast_expr(
4584    ast: &mut ast::Node<ast::Program>,
4585    on_object: &crate::front::Object,
4586) -> Result<Option<ast::Expr>, KclError> {
4587    let SourceRef::BackTrace { ranges } = &on_object.source else {
4588        return Ok(None);
4589    };
4590
4591    match &on_object.kind {
4592        ObjectKind::Wall(_) => {
4593            let [sweep_range, segment_range] = ranges.as_slice() else {
4594                return Err(KclError::refactor(format!(
4595                    "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
4596                    ranges.len(),
4597                    on_object.artifact_id
4598                )));
4599            };
4600            let sweep_ref = get_or_insert_ast_reference(
4601                ast,
4602                &SourceRef::Simple {
4603                    range: sweep_range.0,
4604                    node_path: sweep_range.1.clone(),
4605                },
4606                "solid",
4607                None,
4608            )?;
4609            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4610                return Err(KclError::refactor(format!(
4611                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
4612                    on_object.artifact_id
4613                )));
4614            };
4615            let solid_name = solid_name_expr.name.name.clone();
4616            let solid_expr = ast_name_expr(solid_name.clone());
4617            let segment_ref = get_or_insert_ast_reference(
4618                ast,
4619                &SourceRef::Simple {
4620                    range: segment_range.0,
4621                    node_path: segment_range.1.clone(),
4622                },
4623                LINE_VARIABLE,
4624                None,
4625            )?;
4626
4627            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
4628                let ast::Expr::Name(segment_name_expr) = segment_ref else {
4629                    return Err(KclError::refactor(format!(
4630                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
4631                        on_object.artifact_id
4632                    )));
4633                };
4634                create_member_expression(
4635                    create_member_expression(ast_name_expr(region_name), "tags"),
4636                    &segment_name_expr.name.name,
4637                )
4638            } else {
4639                segment_ref
4640            };
4641
4642            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4643        }
4644        ObjectKind::Cap(cap) => {
4645            let [range] = ranges.as_slice() else {
4646                return Err(KclError::refactor(format!(
4647                    "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
4648                    ranges.len(),
4649                    on_object.artifact_id
4650                )));
4651            };
4652            let sweep_ref = get_or_insert_ast_reference(
4653                ast,
4654                &SourceRef::Simple {
4655                    range: range.0,
4656                    node_path: range.1.clone(),
4657                },
4658                "solid",
4659                None,
4660            )?;
4661            let ast::Expr::Name(solid_name_expr) = sweep_ref else {
4662                return Err(KclError::refactor(format!(
4663                    "Could not resolve sweep reference for selected cap: artifact_id={:?}",
4664                    on_object.artifact_id
4665                )));
4666            };
4667            let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
4668            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
4669            let face_expr = match cap.kind {
4670                crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
4671                crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
4672            };
4673
4674            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
4675        }
4676        _ => Ok(None),
4677    }
4678}
4679
4680#[cfg(feature = "artifact-graph")]
4681fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
4682    let mut existing_artifact_ids = scene_objects
4683        .iter()
4684        .map(|object| object.artifact_id)
4685        .collect::<HashSet<_>>();
4686
4687    for artifact in artifact_graph.values() {
4688        match artifact {
4689            Artifact::Wall(wall) => {
4690                if existing_artifact_ids.contains(&wall.id) {
4691                    continue;
4692                }
4693
4694                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
4695                    Artifact::Segment(segment) => Some(segment),
4696                    _ => None,
4697                }) else {
4698                    continue;
4699                };
4700                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
4701                    Artifact::Sweep(sweep) => Some(sweep),
4702                    _ => None,
4703                }) else {
4704                    continue;
4705                };
4706                let source_segment = segment
4707                    .original_seg_id
4708                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
4709                    .and_then(|artifact| match artifact {
4710                        Artifact::Segment(segment) => Some(segment),
4711                        _ => None,
4712                    })
4713                    .unwrap_or(segment);
4714                let id = ObjectId(scene_objects.len());
4715                scene_objects.push(crate::front::Object {
4716                    id,
4717                    kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
4718                    label: Default::default(),
4719                    comments: Default::default(),
4720                    artifact_id: wall.id,
4721                    source: SourceRef::BackTrace {
4722                        ranges: vec![
4723                            (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
4724                            (
4725                                source_segment.code_ref.range,
4726                                Some(source_segment.code_ref.node_path.clone()),
4727                            ),
4728                        ],
4729                    },
4730                });
4731                existing_artifact_ids.insert(wall.id);
4732            }
4733            Artifact::Cap(cap) => {
4734                if existing_artifact_ids.contains(&cap.id) {
4735                    continue;
4736                }
4737
4738                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
4739                    Artifact::Sweep(sweep) => Some(sweep),
4740                    _ => None,
4741                }) else {
4742                    continue;
4743                };
4744                let id = ObjectId(scene_objects.len());
4745                let kind = match cap.sub_type {
4746                    CapSubType::Start => crate::frontend::api::CapKind::Start,
4747                    CapSubType::End => crate::frontend::api::CapKind::End,
4748                };
4749                scene_objects.push(crate::front::Object {
4750                    id,
4751                    kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
4752                    label: Default::default(),
4753                    comments: Default::default(),
4754                    artifact_id: cap.id,
4755                    source: SourceRef::BackTrace {
4756                        ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
4757                    },
4758                });
4759                existing_artifact_ids.insert(cap.id);
4760            }
4761            _ => {}
4762        }
4763    }
4764}
4765
4766fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
4767    use crate::engine::PlaneName;
4768
4769    match name {
4770        PlaneName::Xy => ast_name_expr("XY".to_owned()),
4771        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
4772        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
4773        PlaneName::NegXy => negated_plane_ast_expr("XY"),
4774        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
4775        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
4776    }
4777}
4778
4779fn negated_plane_ast_expr(name: &str) -> ast::Expr {
4780    ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
4781        ast::UnaryOperator::Neg,
4782        ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
4783    )))
4784}
4785
4786#[cfg(feature = "artifact-graph")]
4787fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
4788    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4789        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
4790        unlabeled: Some(solid_expr),
4791        arguments: vec![ast::LabeledArg {
4792            label: Some(ast::Identifier::new("face")),
4793            arg: face_expr,
4794        }],
4795        digest: None,
4796        non_code_meta: Default::default(),
4797    })))
4798}
4799
4800#[cfg(feature = "artifact-graph")]
4801fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
4802    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
4803        return None;
4804    };
4805    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
4806        return None;
4807    };
4808    if !matches!(
4809        sweep_call.callee.name.name.as_str(),
4810        "extrude" | "revolve" | "sweep" | "loft"
4811    ) {
4812        return None;
4813    }
4814    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
4815        return None;
4816    };
4817    let candidate = region_name_expr.name.name.clone();
4818    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
4819        return None;
4820    };
4821    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
4822        return None;
4823    };
4824    if region_call.callee.name.name != "region" {
4825        return None;
4826    }
4827    Some(candidate)
4828}
4829
4830/// Return the AST expression referencing the variable at the given source ref.
4831/// If no such variable exists, insert a new variable declaration with the given
4832/// prefix.
4833///
4834/// This may return a complex expression referencing properties of the variable
4835/// (e.g., `line1.start`).
4836fn get_or_insert_ast_reference(
4837    ast: &mut ast::Node<ast::Program>,
4838    source_ref: &SourceRef,
4839    prefix: &str,
4840    property: Option<&str>,
4841) -> Result<ast::Expr, KclError> {
4842    let command = AstMutateCommand::AddVariableDeclaration {
4843        prefix: prefix.to_owned(),
4844    };
4845    let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
4846    let AstMutateCommandReturn::Name(var_name) = ret else {
4847        return Err(KclError::refactor(
4848            "Expected variable name returned from AddVariableDeclaration".to_owned(),
4849        ));
4850    };
4851    let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
4852    let Some(property) = property else {
4853        // No property; just return the variable name.
4854        return Ok(var_expr);
4855    };
4856
4857    Ok(create_member_expression(var_expr, property))
4858}
4859
4860fn mutate_ast_node_by_source_ref(
4861    ast: &mut ast::Node<ast::Program>,
4862    source_ref: &SourceRef,
4863    command: AstMutateCommand,
4864) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4865    let (source_range, node_path) = match source_ref {
4866        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
4867        SourceRef::BackTrace { ranges } => {
4868            let [range] = ranges.as_slice() else {
4869                return Err(KclError::refactor(format!(
4870                    "Expected single source ref, got {}; ranges={ranges:#?}",
4871                    ranges.len(),
4872                )));
4873            };
4874            (range.0, range.1.clone())
4875        }
4876    };
4877    let mut context = AstMutateContext {
4878        source_range,
4879        node_path,
4880        command,
4881        defined_names_stack: Default::default(),
4882    };
4883    let control = dfs_mut(ast, &mut context);
4884    match control {
4885        ControlFlow::Continue(_) => Err(KclError::refactor(
4886            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
4887        )),
4888        ControlFlow::Break(break_value) => break_value,
4889    }
4890}
4891
4892#[derive(Debug)]
4893struct AstMutateContext {
4894    source_range: SourceRange,
4895    node_path: Option<ast::NodePath>,
4896    command: AstMutateCommand,
4897    defined_names_stack: Vec<HashSet<String>>,
4898}
4899
4900#[derive(Debug)]
4901#[allow(clippy::large_enum_variant)]
4902enum AstMutateCommand {
4903    /// Add an expression statement to the sketch block.
4904    AddSketchBlockExprStmt {
4905        expr: ast::Expr,
4906    },
4907    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
4908    AddSketchBlockVarDecl {
4909        prefix: String,
4910        expr: ast::Expr,
4911    },
4912    AddVariableDeclaration {
4913        prefix: String,
4914    },
4915    EditPoint {
4916        at: ast::Expr,
4917    },
4918    EditLine {
4919        start: ast::Expr,
4920        end: ast::Expr,
4921        construction: Option<bool>,
4922    },
4923    EditArc {
4924        start: ast::Expr,
4925        end: ast::Expr,
4926        center: ast::Expr,
4927        construction: Option<bool>,
4928    },
4929    EditCircle {
4930        start: ast::Expr,
4931        center: ast::Expr,
4932        construction: Option<bool>,
4933    },
4934    EditConstraintValue {
4935        value: ast::BinaryPart,
4936    },
4937    EditDistanceConstraintLabelPosition {
4938        label_position: ast::Expr,
4939    },
4940    EditCallUnlabeled {
4941        arg: ast::Expr,
4942    },
4943    #[cfg(feature = "artifact-graph")]
4944    EditVarInitialValue {
4945        value: Number,
4946    },
4947    DeleteNode,
4948}
4949
4950impl AstMutateCommand {
4951    fn needs_defined_names_stack(&self) -> bool {
4952        matches!(
4953            self,
4954            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
4955        )
4956    }
4957}
4958
4959#[derive(Debug)]
4960enum AstMutateCommandReturn {
4961    None,
4962    Name(String),
4963}
4964
4965#[derive(Debug, Clone)]
4966struct AstNodeRef {
4967    range: SourceRange,
4968    node_path: Option<ast::NodePath>,
4969}
4970
4971impl<T> From<&ast::Node<T>> for AstNodeRef {
4972    fn from(value: &ast::Node<T>) -> Self {
4973        AstNodeRef {
4974            range: value.into(),
4975            node_path: value.node_path.clone(),
4976        }
4977    }
4978}
4979
4980impl From<&ast::BodyItem> for AstNodeRef {
4981    fn from(value: &ast::BodyItem) -> Self {
4982        match value {
4983            ast::BodyItem::ImportStatement(node) => AstNodeRef {
4984                range: node.into(),
4985                node_path: node.node_path.clone(),
4986            },
4987            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
4988                range: node.into(),
4989                node_path: node.node_path.clone(),
4990            },
4991            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
4992                range: node.into(),
4993                node_path: node.node_path.clone(),
4994            },
4995            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
4996                range: node.into(),
4997                node_path: node.node_path.clone(),
4998            },
4999            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5000                range: node.into(),
5001                node_path: node.node_path.clone(),
5002            },
5003        }
5004    }
5005}
5006
5007impl From<&ast::Expr> for AstNodeRef {
5008    fn from(value: &ast::Expr) -> Self {
5009        AstNodeRef {
5010            range: SourceRange::from(value),
5011            node_path: value.node_path().cloned(),
5012        }
5013    }
5014}
5015
5016impl From<&AstMutateContext> for AstNodeRef {
5017    fn from(value: &AstMutateContext) -> Self {
5018        AstNodeRef {
5019            range: value.source_range,
5020            node_path: value.node_path.clone(),
5021        }
5022    }
5023}
5024
5025impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5026    type Error = crate::walk::AstNodeError;
5027
5028    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5029        Ok(AstNodeRef {
5030            range: SourceRange::try_from(value)?,
5031            node_path: value.try_into()?,
5032        })
5033    }
5034}
5035
5036impl From<AstNodeRef> for SourceRange {
5037    fn from(value: AstNodeRef) -> Self {
5038        value.range
5039    }
5040}
5041
5042impl Visitor for AstMutateContext {
5043    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5044    type Continue = ();
5045
5046    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5047        filter_and_process(self, node)
5048    }
5049
5050    fn finish(&mut self, node: NodeMut<'_>) {
5051        match &node {
5052            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5053                self.defined_names_stack.pop();
5054            }
5055            _ => {}
5056        }
5057    }
5058}
5059
5060fn filter_and_process(
5061    ctx: &mut AstMutateContext,
5062    node: NodeMut,
5063) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5064    let Ok(node_range) = SourceRange::try_from(&node) else {
5065        // Nodes that can't be converted to a range aren't interesting.
5066        return TraversalReturn::new_continue(());
5067    };
5068    // If we're adding a variable declaration, we need to look at variable
5069    // declaration expressions to see if it already has a variable, before
5070    // continuing. The variable declaration's source range won't match the
5071    // target; its init expression will.
5072    if let NodeMut::VariableDeclaration(var_decl) = &node {
5073        let expr_range = SourceRange::from(&var_decl.declaration.init);
5074        let expr_node_path = var_decl.declaration.init.node_path();
5075        if source_ref_matches(ctx, expr_range, expr_node_path) {
5076            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5077                // We found the variable declaration expression. It doesn't need
5078                // to be added.
5079                return TraversalReturn::new_break(Ok((
5080                    AstNodeRef::from(&**var_decl),
5081                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5082                )));
5083            }
5084            if let AstMutateCommand::DeleteNode = &ctx.command {
5085                // We found the variable declaration. Delete the variable along
5086                // with the segment.
5087                return TraversalReturn {
5088                    mutate_body_item: MutateBodyItem::Delete,
5089                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5090                };
5091            }
5092        }
5093    }
5094    // Similar thing with expression statement. We need to look at the
5095    // expression inside it.
5096    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5097        let expr_range = SourceRange::from(&expr_stmt.expression);
5098        let expr_node_path = expr_stmt.expression.node_path();
5099        if source_ref_matches(ctx, expr_range, expr_node_path) {
5100            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5101                // We found the node wrapped in an expression statement. Process
5102                // the statement.
5103                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5104                    return TraversalReturn::new_continue(());
5105                };
5106                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5107            }
5108            if let AstMutateCommand::DeleteNode = &ctx.command {
5109                // We found the node wrapped in an expression statement. Delete
5110                // the whole statement.
5111                return TraversalReturn {
5112                    mutate_body_item: MutateBodyItem::Delete,
5113                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5114                };
5115            }
5116        }
5117    }
5118
5119    if ctx.command.needs_defined_names_stack() {
5120        if let NodeMut::Program(program) = &node {
5121            ctx.defined_names_stack.push(find_defined_names(*program));
5122        } else if let NodeMut::SketchBlock(block) = &node {
5123            ctx.defined_names_stack.push(find_defined_names(&block.body));
5124        }
5125    }
5126
5127    // Make sure the node matches the source ref.
5128    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5129    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5130        return TraversalReturn::new_continue(());
5131    }
5132    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5133        return TraversalReturn::new_continue(());
5134    };
5135    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5136}
5137
5138fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5139    match &ctx.node_path {
5140        Some(target) => Some(target) == node_path,
5141        None => node_range == ctx.source_range,
5142    }
5143}
5144
5145fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5146    match &ctx.command {
5147        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5148            if let NodeMut::SketchBlock(sketch_block) = node {
5149                sketch_block
5150                    .body
5151                    .items
5152                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
5153                        inner: ast::ExpressionStatement {
5154                            expression: expr.clone(),
5155                            digest: None,
5156                        },
5157                        start: Default::default(),
5158                        end: Default::default(),
5159                        module_id: Default::default(),
5160                        node_path: None,
5161                        outer_attrs: Default::default(),
5162                        pre_comments: Default::default(),
5163                        comment_start: Default::default(),
5164                    }));
5165                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5166            }
5167        }
5168        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5169            if let NodeMut::SketchBlock(sketch_block) = node {
5170                let empty_defined_names = HashSet::new();
5171                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5172                let Ok(name) = next_free_name(prefix, defined_names) else {
5173                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5174                };
5175                sketch_block
5176                    .body
5177                    .items
5178                    .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5179                        ast::VariableDeclaration::new(
5180                            ast::VariableDeclarator::new(&name, expr.clone()),
5181                            ast::ItemVisibility::Default,
5182                            ast::VariableKind::Const,
5183                        ),
5184                    ))));
5185                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5186            }
5187        }
5188        AstMutateCommand::AddVariableDeclaration { prefix } => {
5189            if let NodeMut::VariableDeclaration(inner) = node {
5190                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5191            }
5192            if let NodeMut::ExpressionStatement(expr_stmt) = node {
5193                let empty_defined_names = HashSet::new();
5194                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5195                let Ok(name) = next_free_name(prefix, defined_names) else {
5196                    // TODO: Return an error instead?
5197                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5198                };
5199                let mutate_node =
5200                    ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5201                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5202                        ast::ItemVisibility::Default,
5203                        ast::VariableKind::Const,
5204                    ))));
5205                return TraversalReturn {
5206                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5207                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5208                };
5209            }
5210        }
5211        AstMutateCommand::EditPoint { at } => {
5212            if let NodeMut::CallExpressionKw(call) = node {
5213                if call.callee.name.name != POINT_FN {
5214                    return TraversalReturn::new_continue(());
5215                }
5216                // Update the arguments.
5217                for labeled_arg in &mut call.arguments {
5218                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5219                        labeled_arg.arg = at.clone();
5220                    }
5221                }
5222                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5223            }
5224        }
5225        AstMutateCommand::EditLine {
5226            start,
5227            end,
5228            construction,
5229        } => {
5230            if let NodeMut::CallExpressionKw(call) = node {
5231                if call.callee.name.name != LINE_FN {
5232                    return TraversalReturn::new_continue(());
5233                }
5234                // Update the arguments.
5235                for labeled_arg in &mut call.arguments {
5236                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5237                        labeled_arg.arg = start.clone();
5238                    }
5239                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5240                        labeled_arg.arg = end.clone();
5241                    }
5242                }
5243                // Handle construction kwarg
5244                if let Some(construction_value) = construction {
5245                    let construction_exists = call
5246                        .arguments
5247                        .iter()
5248                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5249                    if *construction_value {
5250                        // Add or update construction=true
5251                        if construction_exists {
5252                            // Update existing construction kwarg
5253                            for labeled_arg in &mut call.arguments {
5254                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5255                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5256                                        value: ast::LiteralValue::Bool(true),
5257                                        raw: "true".to_string(),
5258                                        digest: None,
5259                                    })));
5260                                }
5261                            }
5262                        } else {
5263                            // Add new construction kwarg
5264                            call.arguments.push(ast::LabeledArg {
5265                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5266                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5267                                    value: ast::LiteralValue::Bool(true),
5268                                    raw: "true".to_string(),
5269                                    digest: None,
5270                                }))),
5271                            });
5272                        }
5273                    } else {
5274                        // Remove construction kwarg if it exists
5275                        call.arguments
5276                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5277                    }
5278                }
5279                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5280            }
5281        }
5282        AstMutateCommand::EditArc {
5283            start,
5284            end,
5285            center,
5286            construction,
5287        } => {
5288            if let NodeMut::CallExpressionKw(call) = node {
5289                if call.callee.name.name != ARC_FN {
5290                    return TraversalReturn::new_continue(());
5291                }
5292                // Update the arguments.
5293                for labeled_arg in &mut call.arguments {
5294                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5295                        labeled_arg.arg = start.clone();
5296                    }
5297                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5298                        labeled_arg.arg = end.clone();
5299                    }
5300                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5301                        labeled_arg.arg = center.clone();
5302                    }
5303                }
5304                // Handle construction kwarg
5305                if let Some(construction_value) = construction {
5306                    let construction_exists = call
5307                        .arguments
5308                        .iter()
5309                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5310                    if *construction_value {
5311                        // Add or update construction=true
5312                        if construction_exists {
5313                            // Update existing construction kwarg
5314                            for labeled_arg in &mut call.arguments {
5315                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5316                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5317                                        value: ast::LiteralValue::Bool(true),
5318                                        raw: "true".to_string(),
5319                                        digest: None,
5320                                    })));
5321                                }
5322                            }
5323                        } else {
5324                            // Add new construction kwarg
5325                            call.arguments.push(ast::LabeledArg {
5326                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5327                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5328                                    value: ast::LiteralValue::Bool(true),
5329                                    raw: "true".to_string(),
5330                                    digest: None,
5331                                }))),
5332                            });
5333                        }
5334                    } else {
5335                        // Remove construction kwarg if it exists
5336                        call.arguments
5337                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5338                    }
5339                }
5340                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5341            }
5342        }
5343        AstMutateCommand::EditCircle {
5344            start,
5345            center,
5346            construction,
5347        } => {
5348            if let NodeMut::CallExpressionKw(call) = node {
5349                if call.callee.name.name != CIRCLE_FN {
5350                    return TraversalReturn::new_continue(());
5351                }
5352                // Update the arguments.
5353                for labeled_arg in &mut call.arguments {
5354                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5355                        labeled_arg.arg = start.clone();
5356                    }
5357                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5358                        labeled_arg.arg = center.clone();
5359                    }
5360                }
5361                // Handle construction kwarg
5362                if let Some(construction_value) = construction {
5363                    let construction_exists = call
5364                        .arguments
5365                        .iter()
5366                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5367                    if *construction_value {
5368                        if construction_exists {
5369                            for labeled_arg in &mut call.arguments {
5370                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5371                                    labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5372                                        value: ast::LiteralValue::Bool(true),
5373                                        raw: "true".to_string(),
5374                                        digest: None,
5375                                    })));
5376                                }
5377                            }
5378                        } else {
5379                            call.arguments.push(ast::LabeledArg {
5380                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5381                                arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5382                                    value: ast::LiteralValue::Bool(true),
5383                                    raw: "true".to_string(),
5384                                    digest: None,
5385                                }))),
5386                            });
5387                        }
5388                    } else {
5389                        call.arguments
5390                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5391                    }
5392                }
5393                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5394            }
5395        }
5396        AstMutateCommand::EditConstraintValue { value } => {
5397            if let NodeMut::BinaryExpression(binary_expr) = node {
5398                let left_is_constraint = matches!(
5399                    &binary_expr.left,
5400                    ast::BinaryPart::CallExpressionKw(call)
5401                        if matches!(
5402                            call.callee.name.name.as_str(),
5403                            DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
5404                        )
5405                );
5406                if left_is_constraint {
5407                    binary_expr.right = value.clone();
5408                } else {
5409                    binary_expr.left = value.clone();
5410                }
5411
5412                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5413            }
5414        }
5415        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
5416            if let NodeMut::BinaryExpression(binary_expr) = node {
5417                let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
5418                    return TraversalReturn::new_continue(());
5419                };
5420                if !matches!(
5421                    call.callee.name.name.as_str(),
5422                    DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
5423                ) {
5424                    return TraversalReturn::new_continue(());
5425                }
5426
5427                if let Some(label_arg) = call
5428                    .arguments
5429                    .iter_mut()
5430                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
5431                {
5432                    label_arg.arg = label_position.clone();
5433                } else {
5434                    call.arguments.push(ast::LabeledArg {
5435                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
5436                        arg: label_position.clone(),
5437                    });
5438                }
5439
5440                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5441            }
5442        }
5443        AstMutateCommand::EditCallUnlabeled { arg } => {
5444            if let NodeMut::CallExpressionKw(call) = node {
5445                call.unlabeled = Some(arg.clone());
5446                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5447            }
5448        }
5449        #[cfg(feature = "artifact-graph")]
5450        AstMutateCommand::EditVarInitialValue { value } => {
5451            if let NodeMut::NumericLiteral(numeric_literal) = node {
5452                // Update the initial value.
5453                let Ok(literal) = to_source_number(*value) else {
5454                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
5455                        "Could not convert number to AST literal: {:?}",
5456                        *value
5457                    ))));
5458                };
5459                *numeric_literal = ast::Node::no_src(literal);
5460                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5461            }
5462        }
5463        AstMutateCommand::DeleteNode => {
5464            return TraversalReturn {
5465                mutate_body_item: MutateBodyItem::Delete,
5466                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
5467            };
5468        }
5469    }
5470    TraversalReturn::new_continue(())
5471}
5472
5473struct FindSketchBlockSourceRange {
5474    /// The source range of the sketch block before mutation.
5475    target_before_mutation: SourceRange,
5476    /// The source range of the sketch block's last body item after mutation. We
5477    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
5478    /// shared reference.
5479    found: Cell<Option<AstNodeRef>>,
5480}
5481
5482impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
5483    type Error = crate::front::Error;
5484
5485    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5486        let Ok(node_range) = SourceRange::try_from(&node) else {
5487            return Ok(true);
5488        };
5489
5490        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5491            if node_range.module_id() == self.target_before_mutation.module_id()
5492                && node_range.start() == self.target_before_mutation.start()
5493                // End shouldn't match since we added something.
5494                && node_range.end() >= self.target_before_mutation.end()
5495            {
5496                self.found.set(sketch_block.body.items.last().map(|item| match item {
5497                    // For declarations like `circle1 = circle(...)`, use
5498                    // the init expression range so lookup in source_range_to_object
5499                    // matches the segment source range.
5500                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5501                    _ => AstNodeRef::from(item),
5502                }));
5503                return Ok(false);
5504            } else {
5505                // We found a different sketch block. No need to descend into
5506                // its children since sketch blocks cannot be nested.
5507                return Ok(true);
5508            }
5509        }
5510
5511        for child in node.children().iter() {
5512            if !child.visit(*self)? {
5513                return Ok(false);
5514            }
5515        }
5516
5517        Ok(true)
5518    }
5519}
5520
5521struct FindSketchBlockByNodePath {
5522    /// The Node Path of the sketch block before mutation.
5523    target_node_path: ast::NodePath,
5524    /// The ref of the sketch block's last body item after mutation. We need to
5525    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
5526    /// reference.
5527    found: Cell<Option<AstNodeRef>>,
5528}
5529
5530impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
5531    type Error = crate::front::Error;
5532
5533    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
5534        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
5535            return Ok(true);
5536        };
5537
5538        if let crate::walk::Node::SketchBlock(sketch_block) = node {
5539            if let Some(node_path) = node_path
5540                && node_path == self.target_node_path
5541            {
5542                self.found.set(sketch_block.body.items.last().map(|item| match item {
5543                    // For declarations like `circle1 = circle(...)`, use
5544                    // the init expression range so lookup in source_range_to_object
5545                    // matches the segment source range.
5546                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
5547                    _ => AstNodeRef::from(item),
5548                }));
5549
5550                return Ok(false);
5551            } else {
5552                // We found a different sketch block. No need to descend into
5553                // its children since sketch blocks cannot be nested.
5554                return Ok(true);
5555            }
5556        }
5557
5558        for child in node.children().iter() {
5559            if !child.visit(*self)? {
5560                return Ok(false);
5561            }
5562        }
5563
5564        Ok(true)
5565    }
5566}
5567
5568/// After adding an item to a sketch block, find the sketch block, and get the
5569/// source range of the added item. We assume that the added item is the last
5570/// item in the sketch block and that the sketch block's source range has grown,
5571/// but not moved from its starting offset.
5572///
5573/// TODO: Do we need to format *before* mutation in case formatting moves the
5574/// sketch block forward?
5575fn find_sketch_block_added_item(
5576    ast: &ast::Node<ast::Program>,
5577    sketch_block_before_mutation: &AstNodeRef,
5578) -> Result<AstNodeRef, KclError> {
5579    if let Some(node_path) = &sketch_block_before_mutation.node_path {
5580        let find = FindSketchBlockByNodePath {
5581            target_node_path: node_path.clone(),
5582            found: Cell::new(None),
5583        };
5584        let node = crate::walk::Node::from(ast);
5585        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5586        find.found.into_inner().ok_or_else(|| {
5587            KclError::refactor(format!(
5588                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
5589            ))
5590        })
5591    } else {
5592        // No NodePath. Fall back to legacy source range.
5593        let find = FindSketchBlockSourceRange {
5594            target_before_mutation: sketch_block_before_mutation.range,
5595            found: Cell::new(None),
5596        };
5597        let node = crate::walk::Node::from(ast);
5598        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
5599        find.found.into_inner().ok_or_else(|| KclError::refactor(
5600            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?"),
5601        ))
5602    }
5603}
5604
5605fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
5606    // TODO: Don't duplicate this from lib.rs Program.
5607    ast.recast_top(&Default::default(), 0)
5608}
5609
5610pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
5611    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
5612        inner: ast::ArrayExpression {
5613            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
5614            non_code_meta: Default::default(),
5615            digest: None,
5616        },
5617        start: Default::default(),
5618        end: Default::default(),
5619        module_id: Default::default(),
5620        node_path: None,
5621        outer_attrs: Default::default(),
5622        pre_comments: Default::default(),
5623        comment_start: Default::default(),
5624    })))
5625}
5626
5627fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
5628    Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
5629        ast::ArrayExpression {
5630            elements: vec![
5631                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5632                    point.x,
5633                )?)))),
5634                ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5635                    point.y,
5636                )?)))),
5637            ],
5638            non_code_meta: Default::default(),
5639            digest: None,
5640        },
5641    ))))
5642}
5643
5644fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
5645    match expr {
5646        Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
5647            inner: ast::Literal::from(to_source_number(*number)?),
5648            start: Default::default(),
5649            end: Default::default(),
5650            module_id: Default::default(),
5651            node_path: None,
5652            outer_attrs: Default::default(),
5653            pre_comments: Default::default(),
5654            comment_start: Default::default(),
5655        }))),
5656        Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
5657            inner: ast::SketchVar {
5658                initial: Some(Box::new(ast::Node {
5659                    inner: to_source_number(*number)?,
5660                    start: Default::default(),
5661                    end: Default::default(),
5662                    module_id: Default::default(),
5663                    node_path: None,
5664                    outer_attrs: Default::default(),
5665                    pre_comments: Default::default(),
5666                    comment_start: Default::default(),
5667                })),
5668                digest: None,
5669            },
5670            start: Default::default(),
5671            end: Default::default(),
5672            module_id: Default::default(),
5673            node_path: None,
5674            outer_attrs: Default::default(),
5675            pre_comments: Default::default(),
5676            comment_start: Default::default(),
5677        }))),
5678        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
5679    }
5680}
5681
5682fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
5683    Ok(ast::NumericLiteral {
5684        value: number.value,
5685        suffix: number.units,
5686        raw: format_number_literal(number.value, number.units, None)?,
5687        digest: None,
5688    })
5689}
5690
5691pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
5692    ast::Expr::Name(Box::new(ast_name(name)))
5693}
5694
5695fn ast_name(name: String) -> ast::Node<ast::Name> {
5696    ast::Node {
5697        inner: ast::Name {
5698            name: ast::Node {
5699                inner: ast::Identifier { name, digest: None },
5700                start: Default::default(),
5701                end: Default::default(),
5702                module_id: Default::default(),
5703                node_path: None,
5704                outer_attrs: Default::default(),
5705                pre_comments: Default::default(),
5706                comment_start: Default::default(),
5707            },
5708            path: Vec::new(),
5709            abs_path: false,
5710            digest: None,
5711        },
5712        start: Default::default(),
5713        end: Default::default(),
5714        module_id: Default::default(),
5715        node_path: None,
5716        outer_attrs: Default::default(),
5717        pre_comments: Default::default(),
5718        comment_start: Default::default(),
5719    }
5720}
5721
5722pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
5723    ast::Name {
5724        name: ast::Node {
5725            inner: ast::Identifier {
5726                name: name.to_owned(),
5727                digest: None,
5728            },
5729            start: Default::default(),
5730            end: Default::default(),
5731            module_id: Default::default(),
5732            node_path: None,
5733            outer_attrs: Default::default(),
5734            pre_comments: Default::default(),
5735            comment_start: Default::default(),
5736        },
5737        path: Default::default(),
5738        abs_path: false,
5739        digest: None,
5740    }
5741}
5742
5743// Shared AST creation helpers used by both frontend and transpiler to ensure consistency.
5744
5745/// Create an AST node for coincident([expr1, expr2, ...])
5746pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
5747    let elements = exprs.into_iter().collect::<Vec<_>>();
5748    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
5749
5750    // Create array [expr1, expr2, ...]
5751    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5752        elements,
5753        digest: None,
5754        non_code_meta: Default::default(),
5755    })));
5756
5757    // Create coincident([...])
5758    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5759        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
5760        unlabeled: Some(array_expr),
5761        arguments: Default::default(),
5762        digest: None,
5763        non_code_meta: Default::default(),
5764    })))
5765}
5766
5767/// Create an AST node for line(start = [...], end = [...])
5768pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
5769    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5770        callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
5771        unlabeled: None,
5772        arguments: vec![
5773            ast::LabeledArg {
5774                label: Some(ast::Identifier::new(LINE_START_PARAM)),
5775                arg: start_ast,
5776            },
5777            ast::LabeledArg {
5778                label: Some(ast::Identifier::new(LINE_END_PARAM)),
5779                arg: end_ast,
5780            },
5781        ],
5782        digest: None,
5783        non_code_meta: Default::default(),
5784    })))
5785}
5786
5787/// Create an AST node for arc(start = [...], end = [...], center = [...])
5788pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5789    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5790        callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
5791        unlabeled: None,
5792        arguments: vec![
5793            ast::LabeledArg {
5794                label: Some(ast::Identifier::new(ARC_START_PARAM)),
5795                arg: start_ast,
5796            },
5797            ast::LabeledArg {
5798                label: Some(ast::Identifier::new(ARC_END_PARAM)),
5799                arg: end_ast,
5800            },
5801            ast::LabeledArg {
5802                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
5803                arg: center_ast,
5804            },
5805        ],
5806        digest: None,
5807        non_code_meta: Default::default(),
5808    })))
5809}
5810
5811/// Create an AST node for circle(start = [...], center = [...])
5812pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
5813    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5814        callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
5815        unlabeled: None,
5816        arguments: vec![
5817            ast::LabeledArg {
5818                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
5819                arg: start_ast,
5820            },
5821            ast::LabeledArg {
5822                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
5823                arg: center_ast,
5824            },
5825        ],
5826        digest: None,
5827        non_code_meta: Default::default(),
5828    })))
5829}
5830
5831/// Create an AST node for horizontal(line)
5832pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
5833    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5834        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
5835        unlabeled: Some(line_expr),
5836        arguments: Default::default(),
5837        digest: None,
5838        non_code_meta: Default::default(),
5839    })))
5840}
5841
5842/// Create an AST node for vertical(line)
5843pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
5844    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5845        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
5846        unlabeled: Some(line_expr),
5847        arguments: Default::default(),
5848        digest: None,
5849        non_code_meta: Default::default(),
5850    })))
5851}
5852
5853/// Create a member expression like object.property (e.g., line1.end)
5854pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
5855    ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
5856        object: object_expr,
5857        property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
5858            name: ast::Node::no_src(ast::Identifier {
5859                name: property.to_string(),
5860                digest: None,
5861            }),
5862            path: Vec::new(),
5863            abs_path: false,
5864            digest: None,
5865        }))),
5866        computed: false,
5867        digest: None,
5868    })))
5869}
5870
5871/// Create an AST node for `fixed([point, [x, y]])`.
5872fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
5873    // Create [x, y] array literal.
5874    let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5875        position.x,
5876    )?))));
5877    let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
5878        position.y,
5879    )?))));
5880    let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5881        elements: vec![x_literal, y_literal],
5882        digest: None,
5883        non_code_meta: Default::default(),
5884    })));
5885
5886    // Create [point, [x, y]] outer array.
5887    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5888        elements: vec![point_expr, point_array],
5889        digest: None,
5890        non_code_meta: Default::default(),
5891    })));
5892
5893    // Create fixed([...])
5894    Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
5895        ast::CallExpressionKw {
5896            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
5897            unlabeled: Some(array_expr),
5898            arguments: Default::default(),
5899            digest: None,
5900            non_code_meta: Default::default(),
5901        },
5902    ))))
5903}
5904
5905/// Create an AST node for equalLength([line1, line2, ...])
5906pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
5907    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5908        elements: line_exprs,
5909        digest: None,
5910        non_code_meta: Default::default(),
5911    })));
5912
5913    // Create equalLength([...])
5914    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5915        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
5916        unlabeled: Some(array_expr),
5917        arguments: Default::default(),
5918        digest: None,
5919        non_code_meta: Default::default(),
5920    })))
5921}
5922
5923/// Create an AST node for equalRadius([seg1, seg2, ...])
5924pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
5925    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5926        elements: segment_exprs,
5927        digest: None,
5928        non_code_meta: Default::default(),
5929    })));
5930
5931    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5932        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
5933        unlabeled: Some(array_expr),
5934        arguments: Default::default(),
5935        digest: None,
5936        non_code_meta: Default::default(),
5937    })))
5938}
5939
5940/// Create an AST node for tangent([seg1, seg2])
5941pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
5942    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5943        elements: vec![seg1_expr, seg2_expr],
5944        digest: None,
5945        non_code_meta: Default::default(),
5946    })));
5947
5948    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5949        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
5950        unlabeled: Some(array_expr),
5951        arguments: Default::default(),
5952        digest: None,
5953        non_code_meta: Default::default(),
5954    })))
5955}
5956
5957/// Create an AST node for symmetric([input1, input2], axis = line)
5958pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
5959    let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
5960        elements: input_exprs,
5961        digest: None,
5962        non_code_meta: Default::default(),
5963    })));
5964    let arguments = vec![ast::LabeledArg {
5965        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
5966        arg: axis_expr,
5967    }];
5968
5969    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5970        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
5971        unlabeled: Some(array_expr),
5972        arguments,
5973        digest: None,
5974        non_code_meta: Default::default(),
5975    })))
5976}
5977
5978/// Create an AST node for midpoint(segment, point = point)
5979pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
5980    let arguments = vec![ast::LabeledArg {
5981        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
5982        arg: point_expr,
5983    }];
5984
5985    ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5986        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
5987        unlabeled: Some(segment_expr),
5988        arguments,
5989        digest: None,
5990        non_code_meta: Default::default(),
5991    })))
5992}
5993
5994#[cfg(all(feature = "artifact-graph", test))]
5995mod tests {
5996    use super::*;
5997    use crate::engine::PlaneName;
5998    use crate::execution::cache::SketchModeState;
5999    use crate::execution::cache::clear_mem_cache;
6000    use crate::execution::cache::read_old_memory;
6001    use crate::execution::cache::write_old_memory;
6002    use crate::front::Distance;
6003    use crate::front::Fixed;
6004    use crate::front::FixedPoint;
6005    use crate::front::Midpoint;
6006    use crate::front::Object;
6007    use crate::front::Plane;
6008    use crate::front::Sketch;
6009    use crate::front::Tangent;
6010    use crate::frontend::sketch::Vertical;
6011    use crate::pretty::NumericSuffix;
6012
6013    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6014        for object in &scene_graph.objects {
6015            if let ObjectKind::Sketch(_) = &object.kind {
6016                return Some(object);
6017            }
6018        }
6019        None
6020    }
6021
6022    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6023        for object in &scene_graph.objects {
6024            if let ObjectKind::Face(_) = &object.kind {
6025                return Some(object);
6026            }
6027        }
6028        None
6029    }
6030
6031    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6032        for object in &scene_graph.objects {
6033            if matches!(&object.kind, ObjectKind::Wall(_)) {
6034                return Some(object.id);
6035            }
6036        }
6037        None
6038    }
6039
6040    #[test]
6041    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6042        let source = "\
6043region001 = region(point = [0.1, 0.1], sketch = s)
6044extrude001 = extrude(region001, length = 5)
6045revolve001 = revolve(region001, axis = Y)
6046sweep001 = sweep(region001, path = path001)
6047loft001 = loft(region001)
6048not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6049";
6050
6051        let program = Program::parse(source).unwrap().0.unwrap();
6052
6053        assert_eq!(
6054            region_name_from_sweep_variable(&program.ast, "extrude001"),
6055            Some("region001".to_owned())
6056        );
6057        assert_eq!(
6058            region_name_from_sweep_variable(&program.ast, "revolve001"),
6059            Some("region001".to_owned())
6060        );
6061        assert_eq!(
6062            region_name_from_sweep_variable(&program.ast, "sweep001"),
6063            Some("region001".to_owned())
6064        );
6065        assert_eq!(
6066            region_name_from_sweep_variable(&program.ast, "loft001"),
6067            Some("region001".to_owned())
6068        );
6069        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6070    }
6071
6072    #[track_caller]
6073    fn expect_sketch(object: &Object) -> &Sketch {
6074        if let ObjectKind::Sketch(sketch) = &object.kind {
6075            sketch
6076        } else {
6077            panic!("Object is not a sketch: {:?}", object);
6078        }
6079    }
6080
6081    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6082        let point_object = scene_graph.objects.get(point_id.0).unwrap();
6083        let ObjectKind::Segment {
6084            segment: Segment::Point(point),
6085        } = &point_object.kind
6086        else {
6087            panic!("Object is not a point segment: {point_object:?}");
6088        };
6089        point.position.clone()
6090    }
6091
6092    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6093        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6094        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6095    }
6096
6097    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6098        LineCtor {
6099            start: Point2d {
6100                x: Expr::Number(Number { value: start_x, units }),
6101                y: Expr::Number(Number { value: start_y, units }),
6102            },
6103            end: Point2d {
6104                x: Expr::Number(Number { value: end_x, units }),
6105                y: Expr::Number(Number { value: end_y, units }),
6106            },
6107            construction: None,
6108        }
6109    }
6110
6111    async fn create_sketch_with_single_line(
6112        frontend: &mut FrontendState,
6113        ctx: &ExecutorContext,
6114        mock_ctx: &ExecutorContext,
6115        version: Version,
6116    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6117        frontend.program = Program::empty();
6118
6119        let sketch_args = SketchCtor {
6120            on: Plane::Default(PlaneName::Xy),
6121        };
6122        let (_src_delta, _scene_delta, sketch_id) = frontend
6123            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6124            .await
6125            .unwrap();
6126
6127        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
6128        let (source_delta, scene_graph_delta) = frontend
6129            .add_segment(mock_ctx, version, sketch_id, segment, None)
6130            .await
6131            .unwrap();
6132        let line_id = *scene_graph_delta
6133            .new_objects
6134            .last()
6135            .expect("Expected line object id to be created");
6136
6137        (sketch_id, line_id, source_delta, scene_graph_delta)
6138    }
6139
6140    #[tokio::test(flavor = "multi_thread")]
6141    async fn test_sketch_checkpoint_round_trip_restores_state() {
6142        let mut frontend = FrontendState::new();
6143        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6144        let mock_ctx = ExecutorContext::new_mock(None).await;
6145        let version = Version(0);
6146
6147        let (sketch_id, line_id, source_delta, scene_graph_delta) =
6148            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6149
6150        let expected_source = source_delta.text.clone();
6151        let expected_scene_graph = frontend.scene_graph.clone();
6152        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
6153        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
6154
6155        let checkpoint_id = frontend
6156            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6157            .await
6158            .unwrap();
6159
6160        let edited_segments = vec![ExistingSegmentCtor {
6161            id: line_id,
6162            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
6163        }];
6164        let (edited_source, _edited_scene) = frontend
6165            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
6166            .await
6167            .unwrap();
6168        assert_ne!(edited_source.text, expected_source);
6169
6170        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
6171
6172        assert_eq!(restored.source_delta.text, expected_source);
6173        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
6174        assert!(restored.scene_graph_delta.invalidates_ids);
6175        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
6176        assert_eq!(frontend.scene_graph, expected_scene_graph);
6177        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
6178
6179        ctx.close().await;
6180        mock_ctx.close().await;
6181    }
6182
6183    #[tokio::test(flavor = "multi_thread")]
6184    async fn test_sketch_checkpoints_prune_oldest_entries() {
6185        let mut frontend = FrontendState::new();
6186        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6187        let mock_ctx = ExecutorContext::new_mock(None).await;
6188        let version = Version(0);
6189
6190        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6191            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6192
6193        let mut checkpoint_ids = Vec::new();
6194        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
6195            checkpoint_ids.push(
6196                frontend
6197                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6198                    .await
6199                    .unwrap(),
6200            );
6201        }
6202
6203        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
6204        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
6205
6206        let oldest_retained = checkpoint_ids[3];
6207        assert_eq!(
6208            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
6209            Some(oldest_retained)
6210        );
6211
6212        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
6213        assert!(evicted_restore.is_err());
6214        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
6215
6216        frontend
6217            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
6218            .await
6219            .unwrap();
6220
6221        ctx.close().await;
6222        mock_ctx.close().await;
6223    }
6224
6225    #[tokio::test(flavor = "multi_thread")]
6226    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
6227        let mut frontend = FrontendState::new();
6228        let missing_checkpoint = SketchCheckpointId::new(999);
6229
6230        let err = frontend
6231            .restore_sketch_checkpoint(missing_checkpoint)
6232            .await
6233            .expect_err("Expected restore to fail for missing checkpoint");
6234
6235        assert!(err.msg.contains("Sketch checkpoint not found"));
6236    }
6237
6238    #[tokio::test(flavor = "multi_thread")]
6239    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
6240        let mut frontend = FrontendState::new();
6241        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6242        let mock_ctx = ExecutorContext::new_mock(None).await;
6243        let version = Version(0);
6244
6245        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6246            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6247
6248        let checkpoint_a = frontend
6249            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6250            .await
6251            .unwrap();
6252        let checkpoint_b = frontend
6253            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6254            .await
6255            .unwrap();
6256        assert_eq!(frontend.sketch_checkpoints.len(), 2);
6257
6258        frontend.clear_sketch_checkpoints();
6259        assert!(frontend.sketch_checkpoints.is_empty());
6260        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
6261        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
6262
6263        ctx.close().await;
6264        mock_ctx.close().await;
6265    }
6266
6267    #[tokio::test(flavor = "multi_thread")]
6268    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
6269        let mut frontend = FrontendState::new();
6270        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6271        let mock_ctx = ExecutorContext::new_mock(None).await;
6272        let version = Version(0);
6273
6274        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
6275            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6276        let old_source = source_delta.text.clone();
6277        let old_checkpoint = frontend
6278            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6279            .await
6280            .unwrap();
6281        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
6282
6283        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
6284            .unwrap()
6285            .0
6286            .unwrap();
6287
6288        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
6289        let SetProgramOutcome::Success {
6290            checkpoint_id: Some(new_checkpoint),
6291            ..
6292        } = result
6293        else {
6294            panic!("Expected Success with a fresh checkpoint baseline");
6295        };
6296
6297        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
6298
6299        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6300        assert_eq!(old_restore.source_delta.text, old_source);
6301
6302        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
6303        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
6304
6305        ctx.close().await;
6306        mock_ctx.close().await;
6307    }
6308
6309    #[tokio::test(flavor = "multi_thread")]
6310    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
6311        let mut frontend = FrontendState::new();
6312        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6313        let mock_ctx = ExecutorContext::new_mock(None).await;
6314        let version = Version(0);
6315
6316        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
6317            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
6318        let old_checkpoint = frontend
6319            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
6320            .await
6321            .unwrap();
6322        let checkpoint_count_before = frontend.sketch_checkpoints.len();
6323
6324        let failing_program = Program::parse(
6325            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
6326        )
6327        .unwrap()
6328        .0
6329        .unwrap();
6330
6331        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
6332        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
6333        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
6334        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
6335
6336        ctx.close().await;
6337        mock_ctx.close().await;
6338    }
6339
6340    #[tokio::test(flavor = "multi_thread")]
6341    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
6342        let mut frontend = FrontendState::new();
6343        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6344
6345        let program = Program::parse(
6346            "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",
6347        )
6348        .unwrap()
6349        .0
6350        .unwrap();
6351        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
6352        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
6353            panic!("Expected successful baseline program execution");
6354        };
6355
6356        clear_mem_cache().await;
6357        assert!(read_old_memory().await.is_none());
6358
6359        let checkpoint_without_mock_memory = frontend
6360            .create_sketch_checkpoint((*exec_outcome).clone())
6361            .await
6362            .unwrap();
6363
6364        write_old_memory(SketchModeState::new_for_tests()).await;
6365        assert!(read_old_memory().await.is_some());
6366
6367        let checkpoint_with_mock_memory = frontend
6368            .create_sketch_checkpoint((*exec_outcome).clone())
6369            .await
6370            .unwrap();
6371
6372        clear_mem_cache().await;
6373        assert!(read_old_memory().await.is_none());
6374
6375        frontend
6376            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
6377            .await
6378            .unwrap();
6379        assert!(read_old_memory().await.is_some());
6380
6381        frontend
6382            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
6383            .await
6384            .unwrap();
6385        assert!(read_old_memory().await.is_none());
6386
6387        ctx.close().await;
6388    }
6389
6390    #[tokio::test(flavor = "multi_thread")]
6391    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
6392        let source = "\
6393sketch(on = XY) {
6394  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
6395}
6396
6397bad = missing_name
6398";
6399        let program = Program::parse(source).unwrap().0.unwrap();
6400
6401        let mut frontend = FrontendState::new();
6402
6403        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6404        let mock_ctx = ExecutorContext::new_mock(None).await;
6405        let version = Version(0);
6406        let project_id = ProjectId(0);
6407        let file_id = FileId(0);
6408
6409        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
6410            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
6411        };
6412
6413        let sketch_id = frontend
6414            .scene_graph
6415            .objects
6416            .iter()
6417            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
6418            .expect("Expected sketch object from errored hack_set_program");
6419
6420        frontend
6421            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
6422            .await
6423            .unwrap();
6424
6425        ctx.close().await;
6426        mock_ctx.close().await;
6427    }
6428
6429    #[tokio::test(flavor = "multi_thread")]
6430    async fn test_new_sketch_add_point_edit_point() {
6431        let program = Program::empty();
6432
6433        let mut frontend = FrontendState::new();
6434        frontend.program = program;
6435
6436        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6437        let mock_ctx = ExecutorContext::new_mock(None).await;
6438        let version = Version(0);
6439
6440        let sketch_args = SketchCtor {
6441            on: Plane::Default(PlaneName::Xy),
6442        };
6443        let (_src_delta, scene_delta, sketch_id) = frontend
6444            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6445            .await
6446            .unwrap();
6447        assert_eq!(sketch_id, ObjectId(1));
6448        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6449        let sketch_object = &scene_delta.new_graph.objects[1];
6450        assert_eq!(sketch_object.id, ObjectId(1));
6451        assert_eq!(
6452            sketch_object.kind,
6453            ObjectKind::Sketch(Sketch {
6454                args: SketchCtor {
6455                    on: Plane::Default(PlaneName::Xy)
6456                },
6457                plane: ObjectId(0),
6458                segments: vec![],
6459                constraints: vec![],
6460            })
6461        );
6462        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6463
6464        let point_ctor = PointCtor {
6465            position: Point2d {
6466                x: Expr::Number(Number {
6467                    value: 1.0,
6468                    units: NumericSuffix::Inch,
6469                }),
6470                y: Expr::Number(Number {
6471                    value: 2.0,
6472                    units: NumericSuffix::Inch,
6473                }),
6474            },
6475        };
6476        let segment = SegmentCtor::Point(point_ctor);
6477        let (src_delta, scene_delta) = frontend
6478            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6479            .await
6480            .unwrap();
6481        assert_eq!(
6482            src_delta.text.as_str(),
6483            "sketch001 = sketch(on = XY) {
6484  point(at = [1in, 2in])
6485}
6486"
6487        );
6488        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
6489        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6490        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6491            assert_eq!(scene_object.id.0, i);
6492        }
6493
6494        let point_id = *scene_delta.new_objects.last().unwrap();
6495
6496        let point_ctor = PointCtor {
6497            position: Point2d {
6498                x: Expr::Number(Number {
6499                    value: 3.0,
6500                    units: NumericSuffix::Inch,
6501                }),
6502                y: Expr::Number(Number {
6503                    value: 4.0,
6504                    units: NumericSuffix::Inch,
6505                }),
6506            },
6507        };
6508        let segments = vec![ExistingSegmentCtor {
6509            id: point_id,
6510            ctor: SegmentCtor::Point(point_ctor),
6511        }];
6512        let (src_delta, scene_delta) = frontend
6513            .edit_segments(&mock_ctx, version, sketch_id, segments)
6514            .await
6515            .unwrap();
6516        assert_eq!(
6517            src_delta.text.as_str(),
6518            "sketch001 = sketch(on = XY) {
6519  point(at = [3in, 4in])
6520}
6521"
6522        );
6523        assert_eq!(scene_delta.new_objects, vec![]);
6524        assert_eq!(scene_delta.new_graph.objects.len(), 3);
6525
6526        ctx.close().await;
6527        mock_ctx.close().await;
6528    }
6529
6530    #[tokio::test(flavor = "multi_thread")]
6531    async fn test_new_sketch_add_line_edit_line() {
6532        let program = Program::empty();
6533
6534        let mut frontend = FrontendState::new();
6535        frontend.program = program;
6536
6537        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6538        let mock_ctx = ExecutorContext::new_mock(None).await;
6539        let version = Version(0);
6540
6541        let sketch_args = SketchCtor {
6542            on: Plane::Default(PlaneName::Xy),
6543        };
6544        let (_src_delta, scene_delta, sketch_id) = frontend
6545            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6546            .await
6547            .unwrap();
6548        assert_eq!(sketch_id, ObjectId(1));
6549        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6550        let sketch_object = &scene_delta.new_graph.objects[1];
6551        assert_eq!(sketch_object.id, ObjectId(1));
6552        assert_eq!(
6553            sketch_object.kind,
6554            ObjectKind::Sketch(Sketch {
6555                args: SketchCtor {
6556                    on: Plane::Default(PlaneName::Xy)
6557                },
6558                plane: ObjectId(0),
6559                segments: vec![],
6560                constraints: vec![],
6561            })
6562        );
6563        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6564
6565        let line_ctor = LineCtor {
6566            start: Point2d {
6567                x: Expr::Number(Number {
6568                    value: 0.0,
6569                    units: NumericSuffix::Mm,
6570                }),
6571                y: Expr::Number(Number {
6572                    value: 0.0,
6573                    units: NumericSuffix::Mm,
6574                }),
6575            },
6576            end: Point2d {
6577                x: Expr::Number(Number {
6578                    value: 10.0,
6579                    units: NumericSuffix::Mm,
6580                }),
6581                y: Expr::Number(Number {
6582                    value: 10.0,
6583                    units: NumericSuffix::Mm,
6584                }),
6585            },
6586            construction: None,
6587        };
6588        let segment = SegmentCtor::Line(line_ctor);
6589        let (src_delta, scene_delta) = frontend
6590            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6591            .await
6592            .unwrap();
6593        assert_eq!(
6594            src_delta.text.as_str(),
6595            "sketch001 = sketch(on = XY) {
6596  line(start = [0mm, 0mm], end = [10mm, 10mm])
6597}
6598"
6599        );
6600        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6601        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6602        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6603            assert_eq!(scene_object.id.0, i);
6604        }
6605
6606        // The new objects are the end points and then the line.
6607        let line = *scene_delta.new_objects.last().unwrap();
6608
6609        let line_ctor = LineCtor {
6610            start: Point2d {
6611                x: Expr::Number(Number {
6612                    value: 1.0,
6613                    units: NumericSuffix::Mm,
6614                }),
6615                y: Expr::Number(Number {
6616                    value: 2.0,
6617                    units: NumericSuffix::Mm,
6618                }),
6619            },
6620            end: Point2d {
6621                x: Expr::Number(Number {
6622                    value: 13.0,
6623                    units: NumericSuffix::Mm,
6624                }),
6625                y: Expr::Number(Number {
6626                    value: 14.0,
6627                    units: NumericSuffix::Mm,
6628                }),
6629            },
6630            construction: None,
6631        };
6632        let segments = vec![ExistingSegmentCtor {
6633            id: line,
6634            ctor: SegmentCtor::Line(line_ctor),
6635        }];
6636        let (src_delta, scene_delta) = frontend
6637            .edit_segments(&mock_ctx, version, sketch_id, segments)
6638            .await
6639            .unwrap();
6640        assert_eq!(
6641            src_delta.text.as_str(),
6642            "sketch001 = sketch(on = XY) {
6643  line(start = [1mm, 2mm], end = [13mm, 14mm])
6644}
6645"
6646        );
6647        assert_eq!(scene_delta.new_objects, vec![]);
6648        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6649
6650        ctx.close().await;
6651        mock_ctx.close().await;
6652    }
6653
6654    #[tokio::test(flavor = "multi_thread")]
6655    async fn test_new_sketch_add_arc_edit_arc() {
6656        let program = Program::empty();
6657
6658        let mut frontend = FrontendState::new();
6659        frontend.program = program;
6660
6661        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6662        let mock_ctx = ExecutorContext::new_mock(None).await;
6663        let version = Version(0);
6664
6665        let sketch_args = SketchCtor {
6666            on: Plane::Default(PlaneName::Xy),
6667        };
6668        let (_src_delta, scene_delta, sketch_id) = frontend
6669            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6670            .await
6671            .unwrap();
6672        assert_eq!(sketch_id, ObjectId(1));
6673        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
6674        let sketch_object = &scene_delta.new_graph.objects[1];
6675        assert_eq!(sketch_object.id, ObjectId(1));
6676        assert_eq!(
6677            sketch_object.kind,
6678            ObjectKind::Sketch(Sketch {
6679                args: SketchCtor {
6680                    on: Plane::Default(PlaneName::Xy),
6681                },
6682                plane: ObjectId(0),
6683                segments: vec![],
6684                constraints: vec![],
6685            })
6686        );
6687        assert_eq!(scene_delta.new_graph.objects.len(), 2);
6688
6689        let arc_ctor = ArcCtor {
6690            start: Point2d {
6691                x: Expr::Var(Number {
6692                    value: 0.0,
6693                    units: NumericSuffix::Mm,
6694                }),
6695                y: Expr::Var(Number {
6696                    value: 0.0,
6697                    units: NumericSuffix::Mm,
6698                }),
6699            },
6700            end: Point2d {
6701                x: Expr::Var(Number {
6702                    value: 10.0,
6703                    units: NumericSuffix::Mm,
6704                }),
6705                y: Expr::Var(Number {
6706                    value: 10.0,
6707                    units: NumericSuffix::Mm,
6708                }),
6709            },
6710            center: Point2d {
6711                x: Expr::Var(Number {
6712                    value: 10.0,
6713                    units: NumericSuffix::Mm,
6714                }),
6715                y: Expr::Var(Number {
6716                    value: 0.0,
6717                    units: NumericSuffix::Mm,
6718                }),
6719            },
6720            construction: None,
6721        };
6722        let segment = SegmentCtor::Arc(arc_ctor);
6723        let (src_delta, scene_delta) = frontend
6724            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6725            .await
6726            .unwrap();
6727        assert_eq!(
6728            src_delta.text.as_str(),
6729            "sketch001 = sketch(on = XY) {
6730  arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
6731}
6732"
6733        );
6734        assert_eq!(
6735            scene_delta.new_objects,
6736            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
6737        );
6738        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
6739            assert_eq!(scene_object.id.0, i);
6740        }
6741        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6742
6743        // The new objects are the end points, the center, and then the arc.
6744        let arc = *scene_delta.new_objects.last().unwrap();
6745
6746        let arc_ctor = ArcCtor {
6747            start: Point2d {
6748                x: Expr::Var(Number {
6749                    value: 1.0,
6750                    units: NumericSuffix::Mm,
6751                }),
6752                y: Expr::Var(Number {
6753                    value: 2.0,
6754                    units: NumericSuffix::Mm,
6755                }),
6756            },
6757            end: Point2d {
6758                x: Expr::Var(Number {
6759                    value: 13.0,
6760                    units: NumericSuffix::Mm,
6761                }),
6762                y: Expr::Var(Number {
6763                    value: 14.0,
6764                    units: NumericSuffix::Mm,
6765                }),
6766            },
6767            center: Point2d {
6768                x: Expr::Var(Number {
6769                    value: 13.0,
6770                    units: NumericSuffix::Mm,
6771                }),
6772                y: Expr::Var(Number {
6773                    value: 2.0,
6774                    units: NumericSuffix::Mm,
6775                }),
6776            },
6777            construction: None,
6778        };
6779        let segments = vec![ExistingSegmentCtor {
6780            id: arc,
6781            ctor: SegmentCtor::Arc(arc_ctor),
6782        }];
6783        let (src_delta, scene_delta) = frontend
6784            .edit_segments(&mock_ctx, version, sketch_id, segments)
6785            .await
6786            .unwrap();
6787        assert_eq!(
6788            src_delta.text.as_str(),
6789            "sketch001 = sketch(on = XY) {
6790  arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
6791}
6792"
6793        );
6794        assert_eq!(scene_delta.new_objects, vec![]);
6795        assert_eq!(scene_delta.new_graph.objects.len(), 6);
6796
6797        ctx.close().await;
6798        mock_ctx.close().await;
6799    }
6800
6801    #[tokio::test(flavor = "multi_thread")]
6802    async fn test_new_sketch_add_circle_edit_circle() {
6803        let program = Program::empty();
6804
6805        let mut frontend = FrontendState::new();
6806        frontend.program = program;
6807
6808        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6809        let mock_ctx = ExecutorContext::new_mock(None).await;
6810        let version = Version(0);
6811
6812        let sketch_args = SketchCtor {
6813            on: Plane::Default(PlaneName::Xy),
6814        };
6815        let (_src_delta, _scene_delta, sketch_id) = frontend
6816            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
6817            .await
6818            .unwrap();
6819
6820        // Add a circle segment.
6821        let circle_ctor = CircleCtor {
6822            start: Point2d {
6823                x: Expr::Var(Number {
6824                    value: 5.0,
6825                    units: NumericSuffix::Mm,
6826                }),
6827                y: Expr::Var(Number {
6828                    value: 0.0,
6829                    units: NumericSuffix::Mm,
6830                }),
6831            },
6832            center: Point2d {
6833                x: Expr::Var(Number {
6834                    value: 0.0,
6835                    units: NumericSuffix::Mm,
6836                }),
6837                y: Expr::Var(Number {
6838                    value: 0.0,
6839                    units: NumericSuffix::Mm,
6840                }),
6841            },
6842            construction: None,
6843        };
6844        let segment = SegmentCtor::Circle(circle_ctor);
6845        let (src_delta, scene_delta) = frontend
6846            .add_segment(&mock_ctx, version, sketch_id, segment, None)
6847            .await
6848            .unwrap();
6849        assert_eq!(
6850            src_delta.text.as_str(),
6851            "sketch001 = sketch(on = XY) {
6852  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6853}
6854"
6855        );
6856        // The new objects are start, center, and then the circle segment.
6857        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
6858        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6859
6860        let circle = *scene_delta.new_objects.last().unwrap();
6861
6862        // Edit the circle segment.
6863        let circle_ctor = CircleCtor {
6864            start: Point2d {
6865                x: Expr::Var(Number {
6866                    value: 10.0,
6867                    units: NumericSuffix::Mm,
6868                }),
6869                y: Expr::Var(Number {
6870                    value: 0.0,
6871                    units: NumericSuffix::Mm,
6872                }),
6873            },
6874            center: Point2d {
6875                x: Expr::Var(Number {
6876                    value: 3.0,
6877                    units: NumericSuffix::Mm,
6878                }),
6879                y: Expr::Var(Number {
6880                    value: 4.0,
6881                    units: NumericSuffix::Mm,
6882                }),
6883            },
6884            construction: None,
6885        };
6886        let segments = vec![ExistingSegmentCtor {
6887            id: circle,
6888            ctor: SegmentCtor::Circle(circle_ctor),
6889        }];
6890        let (src_delta, scene_delta) = frontend
6891            .edit_segments(&mock_ctx, version, sketch_id, segments)
6892            .await
6893            .unwrap();
6894        assert_eq!(
6895            src_delta.text.as_str(),
6896            "sketch001 = sketch(on = XY) {
6897  circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
6898}
6899"
6900        );
6901        assert_eq!(scene_delta.new_objects, vec![]);
6902        assert_eq!(scene_delta.new_graph.objects.len(), 5);
6903
6904        ctx.close().await;
6905        mock_ctx.close().await;
6906    }
6907
6908    #[tokio::test(flavor = "multi_thread")]
6909    async fn test_delete_circle() {
6910        let initial_source = "sketch001 = sketch(on = XY) {
6911  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6912}
6913";
6914
6915        let program = Program::parse(initial_source).unwrap().0.unwrap();
6916        let mut frontend = FrontendState::new();
6917
6918        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6919        let mock_ctx = ExecutorContext::new_mock(None).await;
6920        let version = Version(0);
6921
6922        frontend.hack_set_program(&ctx, program).await.unwrap();
6923        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6924        let sketch_id = sketch_object.id;
6925        let sketch = expect_sketch(sketch_object);
6926
6927        // The sketch should have 3 segments: start point, center point, and the circle.
6928        assert_eq!(sketch.segments.len(), 3);
6929        let circle_id = sketch.segments[2];
6930
6931        // Delete the circle.
6932        let (src_delta, scene_delta) = frontend
6933            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
6934            .await
6935            .unwrap();
6936        assert_eq!(
6937            src_delta.text.as_str(),
6938            "sketch001 = sketch(on = XY) {
6939}
6940"
6941        );
6942        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
6943        let new_sketch = expect_sketch(new_sketch_object);
6944        assert_eq!(new_sketch.segments.len(), 0);
6945
6946        ctx.close().await;
6947        mock_ctx.close().await;
6948    }
6949
6950    #[tokio::test(flavor = "multi_thread")]
6951    async fn test_edit_circle_via_point() {
6952        let initial_source = "sketch001 = sketch(on = XY) {
6953  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6954}
6955";
6956
6957        let program = Program::parse(initial_source).unwrap().0.unwrap();
6958        let mut frontend = FrontendState::new();
6959
6960        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6961        let mock_ctx = ExecutorContext::new_mock(None).await;
6962        let version = Version(0);
6963
6964        frontend.hack_set_program(&ctx, program).await.unwrap();
6965        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
6966        let sketch_id = sketch_object.id;
6967        let sketch = expect_sketch(sketch_object);
6968
6969        // Find the circle segment and its start point.
6970        let circle_id = sketch
6971            .segments
6972            .iter()
6973            .copied()
6974            .find(|seg_id| {
6975                matches!(
6976                    &frontend.scene_graph.objects[seg_id.0].kind,
6977                    ObjectKind::Segment {
6978                        segment: Segment::Circle(_)
6979                    }
6980                )
6981            })
6982            .expect("Expected a circle segment in sketch");
6983        let circle_object = &frontend.scene_graph.objects[circle_id.0];
6984        let ObjectKind::Segment {
6985            segment: Segment::Circle(circle),
6986        } = &circle_object.kind
6987        else {
6988            panic!("Expected circle segment, got: {:?}", circle_object.kind);
6989        };
6990        let start_point_id = circle.start;
6991
6992        // Edit the start point via SegmentCtor::Point.
6993        let segments = vec![ExistingSegmentCtor {
6994            id: start_point_id,
6995            ctor: SegmentCtor::Point(PointCtor {
6996                position: Point2d {
6997                    x: Expr::Var(Number {
6998                        value: 7.0,
6999                        units: NumericSuffix::Mm,
7000                    }),
7001                    y: Expr::Var(Number {
7002                        value: 1.0,
7003                        units: NumericSuffix::Mm,
7004                    }),
7005                },
7006            }),
7007        }];
7008        let (src_delta, _scene_delta) = frontend
7009            .edit_segments(&mock_ctx, version, sketch_id, segments)
7010            .await
7011            .unwrap();
7012        assert_eq!(
7013            src_delta.text.as_str(),
7014            "sketch001 = sketch(on = XY) {
7015  circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7016}
7017"
7018        );
7019
7020        ctx.close().await;
7021        mock_ctx.close().await;
7022    }
7023
7024    #[tokio::test(flavor = "multi_thread")]
7025    async fn test_add_line_when_sketch_block_uses_variable() {
7026        let initial_source = "s = sketch(on = XY) {}
7027";
7028
7029        let program = Program::parse(initial_source).unwrap().0.unwrap();
7030
7031        let mut frontend = FrontendState::new();
7032
7033        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7034        let mock_ctx = ExecutorContext::new_mock(None).await;
7035        let version = Version(0);
7036
7037        frontend.hack_set_program(&ctx, program).await.unwrap();
7038        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7039        let sketch_id = sketch_object.id;
7040
7041        let line_ctor = LineCtor {
7042            start: Point2d {
7043                x: Expr::Number(Number {
7044                    value: 0.0,
7045                    units: NumericSuffix::Mm,
7046                }),
7047                y: Expr::Number(Number {
7048                    value: 0.0,
7049                    units: NumericSuffix::Mm,
7050                }),
7051            },
7052            end: Point2d {
7053                x: Expr::Number(Number {
7054                    value: 10.0,
7055                    units: NumericSuffix::Mm,
7056                }),
7057                y: Expr::Number(Number {
7058                    value: 10.0,
7059                    units: NumericSuffix::Mm,
7060                }),
7061            },
7062            construction: None,
7063        };
7064        let segment = SegmentCtor::Line(line_ctor);
7065        let (src_delta, scene_delta) = frontend
7066            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7067            .await
7068            .unwrap();
7069        assert_eq!(
7070            src_delta.text.as_str(),
7071            "s = sketch(on = XY) {
7072  line(start = [0mm, 0mm], end = [10mm, 10mm])
7073}
7074"
7075        );
7076        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7077        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7078
7079        ctx.close().await;
7080        mock_ctx.close().await;
7081    }
7082
7083    #[tokio::test(flavor = "multi_thread")]
7084    async fn test_new_sketch_add_line_delete_sketch() {
7085        let program = Program::empty();
7086
7087        let mut frontend = FrontendState::new();
7088        frontend.program = program;
7089
7090        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7091        let mock_ctx = ExecutorContext::new_mock(None).await;
7092        let version = Version(0);
7093
7094        let sketch_args = SketchCtor {
7095            on: Plane::Default(PlaneName::Xy),
7096        };
7097        let (_src_delta, scene_delta, sketch_id) = frontend
7098            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7099            .await
7100            .unwrap();
7101        assert_eq!(sketch_id, ObjectId(1));
7102        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7103        let sketch_object = &scene_delta.new_graph.objects[1];
7104        assert_eq!(sketch_object.id, ObjectId(1));
7105        assert_eq!(
7106            sketch_object.kind,
7107            ObjectKind::Sketch(Sketch {
7108                args: SketchCtor {
7109                    on: Plane::Default(PlaneName::Xy)
7110                },
7111                plane: ObjectId(0),
7112                segments: vec![],
7113                constraints: vec![],
7114            })
7115        );
7116        assert_eq!(scene_delta.new_graph.objects.len(), 2);
7117
7118        let line_ctor = LineCtor {
7119            start: Point2d {
7120                x: Expr::Number(Number {
7121                    value: 0.0,
7122                    units: NumericSuffix::Mm,
7123                }),
7124                y: Expr::Number(Number {
7125                    value: 0.0,
7126                    units: NumericSuffix::Mm,
7127                }),
7128            },
7129            end: Point2d {
7130                x: Expr::Number(Number {
7131                    value: 10.0,
7132                    units: NumericSuffix::Mm,
7133                }),
7134                y: Expr::Number(Number {
7135                    value: 10.0,
7136                    units: NumericSuffix::Mm,
7137                }),
7138            },
7139            construction: None,
7140        };
7141        let segment = SegmentCtor::Line(line_ctor);
7142        let (src_delta, scene_delta) = frontend
7143            .add_segment(&mock_ctx, version, sketch_id, segment, None)
7144            .await
7145            .unwrap();
7146        assert_eq!(
7147            src_delta.text.as_str(),
7148            "sketch001 = sketch(on = XY) {
7149  line(start = [0mm, 0mm], end = [10mm, 10mm])
7150}
7151"
7152        );
7153        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7154
7155        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7156        assert_eq!(src_delta.text.as_str(), "");
7157        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7158
7159        ctx.close().await;
7160        mock_ctx.close().await;
7161    }
7162
7163    #[tokio::test(flavor = "multi_thread")]
7164    async fn test_delete_sketch_when_sketch_block_uses_variable() {
7165        let initial_source = "s = sketch(on = XY) {}
7166";
7167
7168        let program = Program::parse(initial_source).unwrap().0.unwrap();
7169
7170        let mut frontend = FrontendState::new();
7171
7172        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7173        let mock_ctx = ExecutorContext::new_mock(None).await;
7174        let version = Version(0);
7175
7176        frontend.hack_set_program(&ctx, program).await.unwrap();
7177        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7178        let sketch_id = sketch_object.id;
7179
7180        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7181        assert_eq!(src_delta.text.as_str(), "");
7182        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7183
7184        ctx.close().await;
7185        mock_ctx.close().await;
7186    }
7187
7188    #[tokio::test(flavor = "multi_thread")]
7189    async fn test_delete_sketch_after_comment() {
7190        let initial_source = "sketch001 = sketch(on = XZ) {
7191}
7192";
7193
7194        let program = Program::parse(initial_source).unwrap().0.unwrap();
7195        let mut frontend = FrontendState::new();
7196
7197        let ctx = ExecutorContext::new_with_engine(
7198            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7199            Default::default(),
7200        );
7201        let version = Version(0);
7202
7203        frontend.hack_set_program(&ctx, program).await.unwrap();
7204        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7205        let sketch_id = sketch_object.id;
7206        let original_source = sketch_object.source.clone();
7207
7208        let commented_source = "// test 1
7209sketch001 = sketch(on = XZ) {
7210}
7211";
7212        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7213        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7214
7215        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
7216        assert_eq!(cached_sketch_object.source, original_source);
7217
7218        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7219        assert!(
7220            !src_delta.text.contains("sketch001"),
7221            "sketch was not deleted: {}",
7222            src_delta.text
7223        );
7224        // The leading line comment must survive deletion.
7225        assert_eq!(src_delta.text.as_str(), "// test 1\n");
7226        assert_eq!(scene_delta.new_graph.objects.len(), 0);
7227
7228        ctx.close().await;
7229    }
7230
7231    #[tokio::test(flavor = "multi_thread")]
7232    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
7233        let initial_source = "sketch001 = sketch(on = XZ) {
7234}
7235foo = 1
7236";
7237
7238        let program = Program::parse(initial_source).unwrap().0.unwrap();
7239        let mut frontend = FrontendState::new();
7240
7241        let ctx = ExecutorContext::new_with_engine(
7242            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
7243            Default::default(),
7244        );
7245        let version = Version(0);
7246
7247        frontend.hack_set_program(&ctx, program).await.unwrap();
7248        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7249        let sketch_id = sketch_object.id;
7250
7251        let commented_source = "// keep me
7252sketch001 = sketch(on = XZ) {
7253}
7254foo = 1
7255";
7256        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
7257        frontend.engine_execute(&ctx, commented_program).await.unwrap();
7258
7259        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
7260        // The leading comment should remain, now attached to the following body item.
7261        assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
7262
7263        ctx.close().await;
7264    }
7265
7266    #[tokio::test(flavor = "multi_thread")]
7267    async fn test_delete_segment_preserves_pre_comment() {
7268        let initial_source = "\
7269sketch(on = XY) {
7270  point(at = [var 1, var 2])
7271  // describe the middle point
7272  point(at = [var 3, var 4])
7273  point(at = [var 5, var 6])
7274}
7275";
7276
7277        let program = Program::parse(initial_source).unwrap().0.unwrap();
7278        let mut frontend = FrontendState::new();
7279
7280        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7281        let mock_ctx = ExecutorContext::new_mock(None).await;
7282        let version = Version(0);
7283
7284        frontend.hack_set_program(&ctx, program).await.unwrap();
7285        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7286        let sketch_id = sketch_object.id;
7287        let sketch = expect_sketch(sketch_object);
7288
7289        let middle_point_id = *sketch.segments.get(1).unwrap();
7290
7291        let (src_delta, _scene_delta) = frontend
7292            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7293            .await
7294            .unwrap();
7295        // The line comment on the line above the deleted point must be preserved.
7296        // It is reattached to the next surviving body item.
7297        assert_eq!(
7298            src_delta.text.as_str(),
7299            "\
7300sketch(on = XY) {
7301  point(at = [var 1mm, var 2mm])
7302  // describe the middle point
7303  point(at = [var 5mm, var 6mm])
7304}
7305"
7306        );
7307
7308        ctx.close().await;
7309        mock_ctx.close().await;
7310    }
7311
7312    #[tokio::test(flavor = "multi_thread")]
7313    async fn test_delete_last_segment_preserves_pre_comment() {
7314        let initial_source = "\
7315sketch(on = XY) {
7316  point(at = [var 1, var 2])
7317  // describe the trailing point
7318  point(at = [var 3, var 4])
7319}
7320";
7321
7322        let program = Program::parse(initial_source).unwrap().0.unwrap();
7323        let mut frontend = FrontendState::new();
7324
7325        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7326        let mock_ctx = ExecutorContext::new_mock(None).await;
7327        let version = Version(0);
7328
7329        frontend.hack_set_program(&ctx, program).await.unwrap();
7330        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7331        let sketch_id = sketch_object.id;
7332        let sketch = expect_sketch(sketch_object);
7333
7334        let last_point_id = *sketch.segments.last().unwrap();
7335
7336        let (src_delta, _scene_delta) = frontend
7337            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
7338            .await
7339            .unwrap();
7340        // No following item to attach to; the comment is kept inside the sketch
7341        // block as trailing non-code metadata so the user does not lose it.
7342        assert_eq!(
7343            src_delta.text.as_str(),
7344            "\
7345sketch(on = XY) {
7346  point(at = [var 1mm, var 2mm])
7347  // describe the trailing point
7348}
7349"
7350        );
7351
7352        ctx.close().await;
7353        mock_ctx.close().await;
7354    }
7355
7356    #[tokio::test(flavor = "multi_thread")]
7357    async fn test_delete_segment_drops_inline_trailing_comment() {
7358        let initial_source = "\
7359sketch(on = XY) {
7360  point(at = [var 1, var 2])
7361  point(at = [var 3, var 4]) // same-line note that gets dropped
7362  point(at = [var 5, var 6])
7363}
7364";
7365
7366        let program = Program::parse(initial_source).unwrap().0.unwrap();
7367        let mut frontend = FrontendState::new();
7368
7369        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7370        let mock_ctx = ExecutorContext::new_mock(None).await;
7371        let version = Version(0);
7372
7373        frontend.hack_set_program(&ctx, program).await.unwrap();
7374        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7375        let sketch_id = sketch_object.id;
7376        let sketch = expect_sketch(sketch_object);
7377
7378        let middle_point_id = *sketch.segments.get(1).unwrap();
7379
7380        let (src_delta, _scene_delta) = frontend
7381            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
7382            .await
7383            .unwrap();
7384        // The same-line trailing comment is removed along with the deleted code.
7385        assert!(
7386            !src_delta.text.contains("same-line note"),
7387            "inline comment should have been removed: {}",
7388            src_delta.text
7389        );
7390
7391        ctx.close().await;
7392        mock_ctx.close().await;
7393    }
7394
7395    #[tokio::test(flavor = "multi_thread")]
7396    async fn test_delete_segments_preserves_block_comments_across_positions() {
7397        // One test exercising several `delete_body_item_preserving_pre_comments`
7398        // branches at once with `/* ... */` block comments:
7399        //   - first point: leading block comment must migrate to the next item.
7400        //   - first point: same-line trailing block comment must be dropped.
7401        //   - middle point: leading block comment must stay attached after migration.
7402        //   - last point: leading block comment, with no surviving next item,
7403        //     must be converted into a trailing NonCodeNode.
7404        let initial_source = "\
7405sketch(on = XY) {
7406  /* above first - moves to middle */
7407  point(at = [var 1, var 2]) /* same-line on first - dropped */
7408  /* above middle - stays */
7409  point(at = [var 3, var 4])
7410  /* above last - moves to trailing meta */
7411  point(at = [var 5, var 6])
7412}
7413";
7414
7415        let program = Program::parse(initial_source).unwrap().0.unwrap();
7416        let mut frontend = FrontendState::new();
7417
7418        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7419        let mock_ctx = ExecutorContext::new_mock(None).await;
7420        let version = Version(0);
7421
7422        frontend.hack_set_program(&ctx, program).await.unwrap();
7423        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7424        let sketch_id = sketch_object.id;
7425        let sketch = expect_sketch(sketch_object);
7426
7427        let first_point_id = *sketch.segments.first().unwrap();
7428        let last_point_id = *sketch.segments.last().unwrap();
7429
7430        let (src_delta, _scene_delta) = frontend
7431            .delete_objects(
7432                &mock_ctx,
7433                version,
7434                sketch_id,
7435                Vec::new(),
7436                vec![first_point_id, last_point_id],
7437            )
7438            .await
7439            .unwrap();
7440        assert_eq!(
7441            src_delta.text.as_str(),
7442            "\
7443sketch(on = XY) {
7444  /* above first - moves to middle */
7445  /* above middle - stays */
7446  point(at = [var 3mm, var 4mm])
7447  /* above last - moves to trailing meta */
7448}
7449"
7450        );
7451
7452        ctx.close().await;
7453        mock_ctx.close().await;
7454    }
7455
7456    #[tokio::test(flavor = "multi_thread")]
7457    async fn test_edit_line_when_editing_its_start_point() {
7458        let initial_source = "\
7459sketch(on = XY) {
7460  line(start = [var 1, var 2], end = [var 3, var 4])
7461}
7462";
7463
7464        let program = Program::parse(initial_source).unwrap().0.unwrap();
7465
7466        let mut frontend = FrontendState::new();
7467
7468        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7469        let mock_ctx = ExecutorContext::new_mock(None).await;
7470        let version = Version(0);
7471
7472        frontend.hack_set_program(&ctx, program).await.unwrap();
7473        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7474        let sketch_id = sketch_object.id;
7475        let sketch = expect_sketch(sketch_object);
7476
7477        let point_id = *sketch.segments.first().unwrap();
7478
7479        let point_ctor = PointCtor {
7480            position: Point2d {
7481                x: Expr::Var(Number {
7482                    value: 5.0,
7483                    units: NumericSuffix::Inch,
7484                }),
7485                y: Expr::Var(Number {
7486                    value: 6.0,
7487                    units: NumericSuffix::Inch,
7488                }),
7489            },
7490        };
7491        let segments = vec![ExistingSegmentCtor {
7492            id: point_id,
7493            ctor: SegmentCtor::Point(point_ctor),
7494        }];
7495        let (src_delta, scene_delta) = frontend
7496            .edit_segments(&mock_ctx, version, sketch_id, segments)
7497            .await
7498            .unwrap();
7499        assert_eq!(
7500            src_delta.text.as_str(),
7501            "\
7502sketch(on = XY) {
7503  line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
7504}
7505"
7506        );
7507        assert_eq!(scene_delta.new_objects, vec![]);
7508        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7509
7510        ctx.close().await;
7511        mock_ctx.close().await;
7512    }
7513
7514    #[tokio::test(flavor = "multi_thread")]
7515    async fn test_edit_line_when_editing_its_end_point() {
7516        let initial_source = "\
7517sketch(on = XY) {
7518  line(start = [var 1, var 2], end = [var 3, var 4])
7519}
7520";
7521
7522        let program = Program::parse(initial_source).unwrap().0.unwrap();
7523
7524        let mut frontend = FrontendState::new();
7525
7526        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7527        let mock_ctx = ExecutorContext::new_mock(None).await;
7528        let version = Version(0);
7529
7530        frontend.hack_set_program(&ctx, program).await.unwrap();
7531        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7532        let sketch_id = sketch_object.id;
7533        let sketch = expect_sketch(sketch_object);
7534        let point_id = *sketch.segments.get(1).unwrap();
7535
7536        let point_ctor = PointCtor {
7537            position: Point2d {
7538                x: Expr::Var(Number {
7539                    value: 5.0,
7540                    units: NumericSuffix::Inch,
7541                }),
7542                y: Expr::Var(Number {
7543                    value: 6.0,
7544                    units: NumericSuffix::Inch,
7545                }),
7546            },
7547        };
7548        let segments = vec![ExistingSegmentCtor {
7549            id: point_id,
7550            ctor: SegmentCtor::Point(point_ctor),
7551        }];
7552        let (src_delta, scene_delta) = frontend
7553            .edit_segments(&mock_ctx, version, sketch_id, segments)
7554            .await
7555            .unwrap();
7556        assert_eq!(
7557            src_delta.text.as_str(),
7558            "\
7559sketch(on = XY) {
7560  line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
7561}
7562"
7563        );
7564        assert_eq!(scene_delta.new_objects, vec![]);
7565        assert_eq!(
7566            scene_delta.new_graph.objects.len(),
7567            5,
7568            "{:#?}",
7569            scene_delta.new_graph.objects
7570        );
7571
7572        ctx.close().await;
7573        mock_ctx.close().await;
7574    }
7575
7576    #[tokio::test(flavor = "multi_thread")]
7577    async fn test_edit_line_with_coincident_feedback() {
7578        let initial_source = "\
7579sketch(on = XY) {
7580  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
7581  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7582  fixed([line1.start, [0, 0]])
7583  coincident([line1.end, line2.start])
7584  equalLength([line1, line2])
7585}
7586";
7587
7588        let program = Program::parse(initial_source).unwrap().0.unwrap();
7589
7590        let mut frontend = FrontendState::new();
7591
7592        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7593        let mock_ctx = ExecutorContext::new_mock(None).await;
7594        let version = Version(0);
7595
7596        frontend.hack_set_program(&ctx, program).await.unwrap();
7597        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7598        let sketch_id = sketch_object.id;
7599        let sketch = expect_sketch(sketch_object);
7600        let line2_end_id = *sketch.segments.get(4).unwrap();
7601
7602        let segments = vec![ExistingSegmentCtor {
7603            id: line2_end_id,
7604            ctor: SegmentCtor::Point(PointCtor {
7605                position: Point2d {
7606                    x: Expr::Var(Number {
7607                        value: 9.0,
7608                        units: NumericSuffix::None,
7609                    }),
7610                    y: Expr::Var(Number {
7611                        value: 10.0,
7612                        units: NumericSuffix::None,
7613                    }),
7614                },
7615            }),
7616        }];
7617        let (src_delta, scene_delta) = frontend
7618            .edit_segments(&mock_ctx, version, sketch_id, segments)
7619            .await
7620            .unwrap();
7621        assert_eq!(
7622            src_delta.text.as_str(),
7623            "\
7624sketch(on = XY) {
7625  line1 = line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
7626  line2 = line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
7627  fixed([line1.start, [0, 0]])
7628  coincident([line1.end, line2.start])
7629  equalLength([line1, line2])
7630}
7631"
7632        );
7633        assert_eq!(
7634            scene_delta.new_graph.objects.len(),
7635            11,
7636            "{:#?}",
7637            scene_delta.new_graph.objects
7638        );
7639
7640        ctx.close().await;
7641        mock_ctx.close().await;
7642    }
7643
7644    #[tokio::test(flavor = "multi_thread")]
7645    async fn test_delete_point_without_var() {
7646        let initial_source = "\
7647sketch(on = XY) {
7648  point(at = [var 1, var 2])
7649  point(at = [var 3, var 4])
7650  point(at = [var 5, var 6])
7651}
7652";
7653
7654        let program = Program::parse(initial_source).unwrap().0.unwrap();
7655
7656        let mut frontend = FrontendState::new();
7657
7658        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7659        let mock_ctx = ExecutorContext::new_mock(None).await;
7660        let version = Version(0);
7661
7662        frontend.hack_set_program(&ctx, program).await.unwrap();
7663        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7664        let sketch_id = sketch_object.id;
7665        let sketch = expect_sketch(sketch_object);
7666
7667        let point_id = *sketch.segments.get(1).unwrap();
7668
7669        let (src_delta, scene_delta) = frontend
7670            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7671            .await
7672            .unwrap();
7673        assert_eq!(
7674            src_delta.text.as_str(),
7675            "\
7676sketch(on = XY) {
7677  point(at = [var 1mm, var 2mm])
7678  point(at = [var 5mm, var 6mm])
7679}
7680"
7681        );
7682        assert_eq!(scene_delta.new_objects, vec![]);
7683        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7684
7685        ctx.close().await;
7686        mock_ctx.close().await;
7687    }
7688
7689    #[tokio::test(flavor = "multi_thread")]
7690    async fn test_delete_point_with_var() {
7691        let initial_source = "\
7692sketch(on = XY) {
7693  point(at = [var 1, var 2])
7694  point1 = point(at = [var 3, var 4])
7695  point(at = [var 5, var 6])
7696}
7697";
7698
7699        let program = Program::parse(initial_source).unwrap().0.unwrap();
7700
7701        let mut frontend = FrontendState::new();
7702
7703        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7704        let mock_ctx = ExecutorContext::new_mock(None).await;
7705        let version = Version(0);
7706
7707        frontend.hack_set_program(&ctx, program).await.unwrap();
7708        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7709        let sketch_id = sketch_object.id;
7710        let sketch = expect_sketch(sketch_object);
7711
7712        let point_id = *sketch.segments.get(1).unwrap();
7713
7714        let (src_delta, scene_delta) = frontend
7715            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
7716            .await
7717            .unwrap();
7718        assert_eq!(
7719            src_delta.text.as_str(),
7720            "\
7721sketch(on = XY) {
7722  point(at = [var 1mm, var 2mm])
7723  point(at = [var 5mm, var 6mm])
7724}
7725"
7726        );
7727        assert_eq!(scene_delta.new_objects, vec![]);
7728        assert_eq!(scene_delta.new_graph.objects.len(), 4);
7729
7730        ctx.close().await;
7731        mock_ctx.close().await;
7732    }
7733
7734    #[tokio::test(flavor = "multi_thread")]
7735    async fn test_delete_multiple_points() {
7736        let initial_source = "\
7737sketch(on = XY) {
7738  point(at = [var 1, var 2])
7739  point1 = point(at = [var 3, var 4])
7740  point(at = [var 5, var 6])
7741}
7742";
7743
7744        let program = Program::parse(initial_source).unwrap().0.unwrap();
7745
7746        let mut frontend = FrontendState::new();
7747
7748        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7749        let mock_ctx = ExecutorContext::new_mock(None).await;
7750        let version = Version(0);
7751
7752        frontend.hack_set_program(&ctx, program).await.unwrap();
7753        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7754        let sketch_id = sketch_object.id;
7755
7756        let sketch = expect_sketch(sketch_object);
7757
7758        let point1_id = *sketch.segments.first().unwrap();
7759        let point2_id = *sketch.segments.get(1).unwrap();
7760
7761        let (src_delta, scene_delta) = frontend
7762            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
7763            .await
7764            .unwrap();
7765        assert_eq!(
7766            src_delta.text.as_str(),
7767            "\
7768sketch(on = XY) {
7769  point(at = [var 5mm, var 6mm])
7770}
7771"
7772        );
7773        assert_eq!(scene_delta.new_objects, vec![]);
7774        assert_eq!(scene_delta.new_graph.objects.len(), 3);
7775
7776        ctx.close().await;
7777        mock_ctx.close().await;
7778    }
7779
7780    #[tokio::test(flavor = "multi_thread")]
7781    async fn test_delete_coincident_constraint() {
7782        let initial_source = "\
7783sketch(on = XY) {
7784  point1 = point(at = [var 1, var 2])
7785  point2 = point(at = [var 3, var 4])
7786  coincident([point1, point2])
7787  point(at = [var 5, var 6])
7788}
7789";
7790
7791        let program = Program::parse(initial_source).unwrap().0.unwrap();
7792
7793        let mut frontend = FrontendState::new();
7794
7795        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7796        let mock_ctx = ExecutorContext::new_mock(None).await;
7797        let version = Version(0);
7798
7799        frontend.hack_set_program(&ctx, program).await.unwrap();
7800        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7801        let sketch_id = sketch_object.id;
7802        let sketch = expect_sketch(sketch_object);
7803
7804        let coincident_id = *sketch.constraints.first().unwrap();
7805
7806        let (src_delta, scene_delta) = frontend
7807            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
7808            .await
7809            .unwrap();
7810        assert_eq!(
7811            src_delta.text.as_str(),
7812            "\
7813sketch(on = XY) {
7814  point1 = point(at = [var 1mm, var 2mm])
7815  point2 = point(at = [var 3mm, var 4mm])
7816  point(at = [var 5mm, var 6mm])
7817}
7818"
7819        );
7820        assert_eq!(scene_delta.new_objects, vec![]);
7821        assert_eq!(scene_delta.new_graph.objects.len(), 5);
7822
7823        ctx.close().await;
7824        mock_ctx.close().await;
7825    }
7826
7827    #[tokio::test(flavor = "multi_thread")]
7828    async fn test_delete_line_cascades_to_coincident_constraint() {
7829        let initial_source = "\
7830sketch(on = XY) {
7831  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7832  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7833  coincident([line1.end, line2.start])
7834}
7835";
7836
7837        let program = Program::parse(initial_source).unwrap().0.unwrap();
7838
7839        let mut frontend = FrontendState::new();
7840
7841        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7842        let mock_ctx = ExecutorContext::new_mock(None).await;
7843        let version = Version(0);
7844
7845        frontend.hack_set_program(&ctx, program).await.unwrap();
7846        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7847        let sketch_id = sketch_object.id;
7848        let sketch = expect_sketch(sketch_object);
7849        let line_id = *sketch.segments.get(5).unwrap();
7850
7851        let (src_delta, scene_delta) = frontend
7852            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7853            .await
7854            .unwrap();
7855        assert_eq!(
7856            src_delta.text.as_str(),
7857            "\
7858sketch(on = XY) {
7859  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7860}
7861"
7862        );
7863        assert_eq!(
7864            scene_delta.new_graph.objects.len(),
7865            5,
7866            "{:#?}",
7867            scene_delta.new_graph.objects
7868        );
7869
7870        ctx.close().await;
7871        mock_ctx.close().await;
7872    }
7873
7874    #[tokio::test(flavor = "multi_thread")]
7875    async fn test_delete_line_cascades_to_distance_constraint() {
7876        let initial_source = "\
7877sketch(on = XY) {
7878  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7879  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7880  distance([line1.end, line2.start]) == 10mm
7881}
7882";
7883
7884        let program = Program::parse(initial_source).unwrap().0.unwrap();
7885
7886        let mut frontend = FrontendState::new();
7887
7888        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7889        let mock_ctx = ExecutorContext::new_mock(None).await;
7890        let version = Version(0);
7891
7892        frontend.hack_set_program(&ctx, program).await.unwrap();
7893        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7894        let sketch_id = sketch_object.id;
7895        let sketch = expect_sketch(sketch_object);
7896        let line_id = *sketch.segments.get(5).unwrap();
7897
7898        let (src_delta, scene_delta) = frontend
7899            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
7900            .await
7901            .unwrap();
7902        assert_eq!(
7903            src_delta.text.as_str(),
7904            "\
7905sketch(on = XY) {
7906  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7907}
7908"
7909        );
7910        assert_eq!(
7911            scene_delta.new_graph.objects.len(),
7912            5,
7913            "{:#?}",
7914            scene_delta.new_graph.objects
7915        );
7916
7917        ctx.close().await;
7918        mock_ctx.close().await;
7919    }
7920
7921    #[tokio::test(flavor = "multi_thread")]
7922    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
7923        let initial_source = "\
7924sketch(on = XY) {
7925  point1 = point(at = [var 1, var 2])
7926  point2 = point(at = [var 3, var 4])
7927  horizontalDistance([point1, point2]) == 10mm
7928}
7929";
7930
7931        let program = Program::parse(initial_source).unwrap().0.unwrap();
7932
7933        let mut frontend = FrontendState::new();
7934
7935        let mock_ctx = ExecutorContext::new_mock(None).await;
7936        let version = Version(0);
7937
7938        frontend.program = program.clone();
7939        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7940        frontend.update_state_after_exec(outcome, true);
7941        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7942        let sketch_id = sketch_object.id;
7943        let sketch = expect_sketch(sketch_object);
7944        let point2_id = *sketch.segments.get(1).unwrap();
7945
7946        let (src_delta, scene_delta) = frontend
7947            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
7948            .await
7949            .unwrap();
7950        assert_eq!(
7951            src_delta.text.as_str(),
7952            "\
7953sketch(on = XY) {
7954  point1 = point(at = [var 1mm, var 2mm])
7955}
7956"
7957        );
7958        assert_eq!(
7959            scene_delta.new_graph.objects.len(),
7960            3,
7961            "{:#?}",
7962            scene_delta.new_graph.objects
7963        );
7964
7965        mock_ctx.close().await;
7966    }
7967
7968    #[tokio::test(flavor = "multi_thread")]
7969    async fn test_delete_line_cascades_to_fixed_constraint() {
7970        let initial_source = "\
7971sketch(on = XY) {
7972  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
7973  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
7974  fixed([line1.start, [0, 0]])
7975}
7976";
7977
7978        let program = Program::parse(initial_source).unwrap().0.unwrap();
7979
7980        let mut frontend = FrontendState::new();
7981
7982        let mock_ctx = ExecutorContext::new_mock(None).await;
7983        let version = Version(0);
7984
7985        frontend.program = program.clone();
7986        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
7987        frontend.update_state_after_exec(outcome, true);
7988        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7989        let sketch_id = sketch_object.id;
7990        let sketch = expect_sketch(sketch_object);
7991        let line1_id = *sketch.segments.get(2).unwrap();
7992
7993        let (src_delta, scene_delta) = frontend
7994            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
7995            .await
7996            .unwrap();
7997        assert_eq!(
7998            src_delta.text.as_str(),
7999            "\
8000sketch(on = XY) {
8001  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8002}
8003"
8004        );
8005        assert_eq!(
8006            scene_delta.new_graph.objects.len(),
8007            5,
8008            "{:#?}",
8009            scene_delta.new_graph.objects
8010        );
8011
8012        mock_ctx.close().await;
8013    }
8014
8015    #[tokio::test(flavor = "multi_thread")]
8016    async fn test_delete_line_cascades_to_midpoint_constraint() {
8017        let initial_source = "\
8018sketch(on = XY) {
8019  point1 = point(at = [var 1, var 2])
8020  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
8021  midpoint(line1, point = point1)
8022}
8023";
8024
8025        let program = Program::parse(initial_source).unwrap().0.unwrap();
8026
8027        let mut frontend = FrontendState::new();
8028
8029        let mock_ctx = ExecutorContext::new_mock(None).await;
8030        let version = Version(0);
8031
8032        frontend.program = program.clone();
8033        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8034        frontend.update_state_after_exec(outcome, true);
8035        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8036        let sketch_id = sketch_object.id;
8037        let sketch = expect_sketch(sketch_object);
8038        let line1_id = *sketch.segments.get(3).unwrap();
8039
8040        let (src_delta, scene_delta) = frontend
8041            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8042            .await
8043            .unwrap();
8044        assert_eq!(
8045            src_delta.text.as_str(),
8046            "\
8047sketch(on = XY) {
8048  point1 = point(at = [var 1mm, var 2mm])
8049}
8050"
8051        );
8052        assert_eq!(
8053            scene_delta.new_graph.objects.len(),
8054            3,
8055            "{:#?}",
8056            scene_delta.new_graph.objects
8057        );
8058
8059        mock_ctx.close().await;
8060    }
8061
8062    #[tokio::test(flavor = "multi_thread")]
8063    async fn test_delete_point_preserves_multiline_coincident_constraint() {
8064        let initial_source = "\
8065sketch(on = XY) {
8066  point1 = point(at = [var 1, var 2])
8067  point2 = point(at = [var 3, var 4])
8068  point3 = point(at = [var 5, var 6])
8069  coincident([point1, point2, point3])
8070}
8071";
8072
8073        let program = Program::parse(initial_source).unwrap().0.unwrap();
8074
8075        let mut frontend = FrontendState::new();
8076
8077        let mock_ctx = ExecutorContext::new_mock(None).await;
8078        let version = Version(0);
8079
8080        frontend.program = program.clone();
8081        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8082        frontend.update_state_after_exec(outcome, true);
8083        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8084        let sketch_id = sketch_object.id;
8085        let sketch = expect_sketch(sketch_object);
8086        let point3_id = *sketch.segments.get(2).unwrap();
8087
8088        let (src_delta, scene_delta) = frontend
8089            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
8090            .await
8091            .unwrap();
8092        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
8093        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
8094        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
8095        assert!(
8096            src_delta.text.contains("coincident([point1, point2])"),
8097            "{}",
8098            src_delta.text
8099        );
8100
8101        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8102        let sketch = expect_sketch(sketch_object);
8103        assert_eq!(sketch.segments.len(), 2);
8104        assert_eq!(sketch.constraints.len(), 1);
8105
8106        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8107        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8108            panic!("Expected constraint object");
8109        };
8110        let Constraint::Coincident(coincident) = constraint else {
8111            panic!("Expected coincident constraint");
8112        };
8113        assert_eq!(
8114            coincident.segments,
8115            sketch
8116                .segments
8117                .iter()
8118                .copied()
8119                .map(Into::into)
8120                .collect::<Vec<ConstraintSegment>>()
8121        );
8122
8123        mock_ctx.close().await;
8124    }
8125
8126    #[tokio::test(flavor = "multi_thread")]
8127    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
8128        let initial_source = "\
8129sketch(on = XY) {
8130  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8131  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8132  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8133  equalLength([line1, line2, line3])
8134}
8135";
8136
8137        let program = Program::parse(initial_source).unwrap().0.unwrap();
8138
8139        let mut frontend = FrontendState::new();
8140
8141        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8142        let mock_ctx = ExecutorContext::new_mock(None).await;
8143        let version = Version(0);
8144
8145        frontend.hack_set_program(&ctx, program).await.unwrap();
8146        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8147        let sketch_id = sketch_object.id;
8148        let sketch = expect_sketch(sketch_object);
8149        let line3_id = *sketch.segments.get(8).unwrap();
8150
8151        let (src_delta, scene_delta) = frontend
8152            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8153            .await
8154            .unwrap();
8155        assert_eq!(
8156            src_delta.text.as_str(),
8157            "\
8158sketch(on = XY) {
8159  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8160  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8161  equalLength([line1, line2])
8162}
8163"
8164        );
8165
8166        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8167        let sketch = expect_sketch(sketch_object);
8168        assert_eq!(sketch.constraints.len(), 1);
8169
8170        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8171        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8172            panic!("Expected constraint object");
8173        };
8174        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
8175            panic!("Expected lines equal length constraint");
8176        };
8177        assert_eq!(lines_equal_length.lines.len(), 2);
8178
8179        ctx.close().await;
8180        mock_ctx.close().await;
8181    }
8182
8183    #[tokio::test(flavor = "multi_thread")]
8184    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
8185        let initial_source = "\
8186sketch(on = XY) {
8187  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8188  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8189  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8190  horizontal([line1.end, line2.start, line3.start])
8191}
8192";
8193
8194        let program = Program::parse(initial_source).unwrap().0.unwrap();
8195
8196        let mut frontend = FrontendState::new();
8197
8198        let mock_ctx = ExecutorContext::new_mock(None).await;
8199        let version = Version(0);
8200
8201        frontend.program = program.clone();
8202        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8203        frontend.update_state_after_exec(outcome, true);
8204        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8205        let sketch_id = sketch_object.id;
8206        let sketch = expect_sketch(sketch_object);
8207        let line1_id = *sketch.segments.get(2).unwrap();
8208
8209        let (src_delta, scene_delta) = frontend
8210            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8211            .await
8212            .unwrap();
8213        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8214        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8215        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8216        assert!(
8217            src_delta.text.contains("horizontal([line2.start, line3.start])"),
8218            "{}",
8219            src_delta.text
8220        );
8221
8222        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8223        let sketch = expect_sketch(sketch_object);
8224        assert_eq!(sketch.constraints.len(), 1);
8225
8226        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8227        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8228            panic!("Expected constraint object");
8229        };
8230        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
8231            panic!("Expected horizontal points constraint");
8232        };
8233        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8234        assert_eq!(*points, remaining_points);
8235
8236        mock_ctx.close().await;
8237    }
8238
8239    #[tokio::test(flavor = "multi_thread")]
8240    async fn test_delete_line_preserves_multiline_vertical_constraint() {
8241        let initial_source = "\
8242sketch(on = XY) {
8243  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8244  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8245  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8246  vertical([line1.end, line2.start, line3.start])
8247}
8248";
8249
8250        let program = Program::parse(initial_source).unwrap().0.unwrap();
8251
8252        let mut frontend = FrontendState::new();
8253
8254        let mock_ctx = ExecutorContext::new_mock(None).await;
8255        let version = Version(0);
8256
8257        frontend.program = program.clone();
8258        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8259        frontend.update_state_after_exec(outcome, true);
8260        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8261        let sketch_id = sketch_object.id;
8262        let sketch = expect_sketch(sketch_object);
8263        let line1_id = *sketch.segments.get(2).unwrap();
8264
8265        let (src_delta, scene_delta) = frontend
8266            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8267            .await
8268            .unwrap();
8269        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8270        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8271        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8272        assert!(
8273            src_delta.text.contains("vertical([line2.start, line3.start])"),
8274            "{}",
8275            src_delta.text
8276        );
8277
8278        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8279        let sketch = expect_sketch(sketch_object);
8280        assert_eq!(sketch.constraints.len(), 1);
8281
8282        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8283        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8284            panic!("Expected constraint object");
8285        };
8286        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
8287            panic!("Expected vertical points constraint");
8288        };
8289        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8290        assert_eq!(*points, remaining_points);
8291
8292        mock_ctx.close().await;
8293    }
8294
8295    #[tokio::test(flavor = "multi_thread")]
8296    async fn test_delete_line_preserves_multiline_coincident_constraint() {
8297        let initial_source = "\
8298sketch(on = XY) {
8299  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8300  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8301  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8302  coincident([line1.end, line2.start, line3.start])
8303}
8304";
8305
8306        let program = Program::parse(initial_source).unwrap().0.unwrap();
8307
8308        let mut frontend = FrontendState::new();
8309
8310        let mock_ctx = ExecutorContext::new_mock(None).await;
8311        let version = Version(0);
8312
8313        frontend.program = program.clone();
8314        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8315        frontend.update_state_after_exec(outcome, true);
8316        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8317        let sketch_id = sketch_object.id;
8318        let sketch = expect_sketch(sketch_object);
8319        let line1_id = *sketch.segments.get(2).unwrap();
8320
8321        let (src_delta, scene_delta) = frontend
8322            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
8323            .await
8324            .unwrap();
8325        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
8326        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
8327        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
8328        assert!(
8329            src_delta.text.contains("coincident([line2.start, line3.start])"),
8330            "{}",
8331            src_delta.text
8332        );
8333
8334        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8335        let sketch = expect_sketch(sketch_object);
8336        assert_eq!(sketch.constraints.len(), 1);
8337
8338        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8339        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8340            panic!("Expected constraint object");
8341        };
8342        let Constraint::Coincident(coincident) = constraint else {
8343            panic!("Expected coincident constraint");
8344        };
8345        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
8346        assert_eq!(coincident.segments, remaining_segments);
8347
8348        mock_ctx.close().await;
8349    }
8350
8351    #[tokio::test(flavor = "multi_thread")]
8352    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
8353        let initial_source = "\
8354sketch(on = XY) {
8355  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8356  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8357  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8358  equalLength([line1, line2, line3])
8359}
8360";
8361
8362        let program = Program::parse(initial_source).unwrap().0.unwrap();
8363
8364        let mut frontend = FrontendState::new();
8365
8366        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8367        let mock_ctx = ExecutorContext::new_mock(None).await;
8368        let version = Version(0);
8369
8370        frontend.hack_set_program(&ctx, program).await.unwrap();
8371        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8372        let sketch_id = sketch_object.id;
8373        let sketch = expect_sketch(sketch_object);
8374        let line2_id = *sketch.segments.get(5).unwrap();
8375        let line3_id = *sketch.segments.get(8).unwrap();
8376
8377        let (src_delta, scene_delta) = frontend
8378            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8379            .await
8380            .unwrap();
8381        assert_eq!(
8382            src_delta.text.as_str(),
8383            "\
8384sketch(on = XY) {
8385  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8386}
8387"
8388        );
8389
8390        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8391        let sketch = expect_sketch(sketch_object);
8392        assert!(sketch.constraints.is_empty());
8393
8394        ctx.close().await;
8395        mock_ctx.close().await;
8396    }
8397
8398    #[tokio::test(flavor = "multi_thread")]
8399    async fn test_delete_line_preserves_multiline_parallel_constraint() {
8400        let initial_source = "\
8401sketch(on = XY) {
8402  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8403  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8404  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8405  parallel([line1, line2, line3])
8406}
8407";
8408
8409        let program = Program::parse(initial_source).unwrap().0.unwrap();
8410
8411        let mut frontend = FrontendState::new();
8412
8413        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8414        let mock_ctx = ExecutorContext::new_mock(None).await;
8415        let version = Version(0);
8416
8417        frontend.hack_set_program(&ctx, program).await.unwrap();
8418        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8419        let sketch_id = sketch_object.id;
8420        let sketch = expect_sketch(sketch_object);
8421        let line3_id = *sketch.segments.get(8).unwrap();
8422
8423        let (src_delta, scene_delta) = frontend
8424            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
8425            .await
8426            .unwrap();
8427        assert_eq!(
8428            src_delta.text.as_str(),
8429            "\
8430sketch(on = XY) {
8431  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8432  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8433  parallel([line1, line2])
8434}
8435"
8436        );
8437
8438        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8439        let sketch = expect_sketch(sketch_object);
8440        assert_eq!(sketch.constraints.len(), 1);
8441
8442        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
8443        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8444            panic!("Expected constraint object");
8445        };
8446        let Constraint::Parallel(parallel) = constraint else {
8447            panic!("Expected parallel constraint");
8448        };
8449        assert_eq!(parallel.lines.len(), 2);
8450
8451        ctx.close().await;
8452        mock_ctx.close().await;
8453    }
8454
8455    #[tokio::test(flavor = "multi_thread")]
8456    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
8457        let initial_source = "\
8458sketch(on = XY) {
8459  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8460  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8461  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
8462  parallel([line1, line2, line3])
8463}
8464";
8465
8466        let program = Program::parse(initial_source).unwrap().0.unwrap();
8467
8468        let mut frontend = FrontendState::new();
8469
8470        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8471        let mock_ctx = ExecutorContext::new_mock(None).await;
8472        let version = Version(0);
8473
8474        frontend.hack_set_program(&ctx, program).await.unwrap();
8475        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8476        let sketch_id = sketch_object.id;
8477        let sketch = expect_sketch(sketch_object);
8478        let line2_id = *sketch.segments.get(5).unwrap();
8479        let line3_id = *sketch.segments.get(8).unwrap();
8480
8481        let (src_delta, scene_delta) = frontend
8482            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
8483            .await
8484            .unwrap();
8485        assert_eq!(
8486            src_delta.text.as_str(),
8487            "\
8488sketch(on = XY) {
8489  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8490}
8491"
8492        );
8493
8494        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8495        let sketch = expect_sketch(sketch_object);
8496        assert!(sketch.constraints.is_empty());
8497
8498        ctx.close().await;
8499        mock_ctx.close().await;
8500    }
8501
8502    #[tokio::test(flavor = "multi_thread")]
8503    async fn test_delete_line_line_coincident_constraint() {
8504        let initial_source = "\
8505sketch(on = XY) {
8506  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8507  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8508  coincident([line1, line2])
8509}
8510";
8511
8512        let program = Program::parse(initial_source).unwrap().0.unwrap();
8513
8514        let mut frontend = FrontendState::new();
8515
8516        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8517        let mock_ctx = ExecutorContext::new_mock(None).await;
8518        let version = Version(0);
8519
8520        frontend.hack_set_program(&ctx, program).await.unwrap();
8521        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8522        let sketch_id = sketch_object.id;
8523        let sketch = expect_sketch(sketch_object);
8524
8525        let coincident_id = *sketch.constraints.first().unwrap();
8526
8527        let (src_delta, scene_delta) = frontend
8528            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
8529            .await
8530            .unwrap();
8531        assert_eq!(
8532            src_delta.text.as_str(),
8533            "\
8534sketch(on = XY) {
8535  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8536  line2 = line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
8537}
8538"
8539        );
8540        assert_eq!(scene_delta.new_objects, vec![]);
8541        assert_eq!(scene_delta.new_graph.objects.len(), 8);
8542
8543        ctx.close().await;
8544        mock_ctx.close().await;
8545    }
8546
8547    #[tokio::test(flavor = "multi_thread")]
8548    async fn test_two_points_coincident() {
8549        let initial_source = "\
8550sketch(on = XY) {
8551  point1 = point(at = [var 1, var 2])
8552  point(at = [3, 4])
8553}
8554";
8555
8556        let program = Program::parse(initial_source).unwrap().0.unwrap();
8557
8558        let mut frontend = FrontendState::new();
8559
8560        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8561        let mock_ctx = ExecutorContext::new_mock(None).await;
8562        let version = Version(0);
8563
8564        frontend.hack_set_program(&ctx, program).await.unwrap();
8565        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8566        let sketch_id = sketch_object.id;
8567        let sketch = expect_sketch(sketch_object);
8568        let point0_id = *sketch.segments.first().unwrap();
8569        let point1_id = *sketch.segments.get(1).unwrap();
8570
8571        let constraint = Constraint::Coincident(Coincident {
8572            segments: vec![point0_id.into(), point1_id.into()],
8573        });
8574        let (src_delta, scene_delta) = frontend
8575            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8576            .await
8577            .unwrap();
8578        assert_eq!(
8579            src_delta.text.as_str(),
8580            "\
8581sketch(on = XY) {
8582  point1 = point(at = [var 1, var 2])
8583  point2 = point(at = [3, 4])
8584  coincident([point1, point2])
8585}
8586"
8587        );
8588        assert_eq!(
8589            scene_delta.new_graph.objects.len(),
8590            5,
8591            "{:#?}",
8592            scene_delta.new_graph.objects
8593        );
8594
8595        ctx.close().await;
8596        mock_ctx.close().await;
8597    }
8598
8599    #[tokio::test(flavor = "multi_thread")]
8600    async fn test_three_points_coincident() {
8601        let initial_source = "\
8602sketch(on = XY) {
8603  point1 = point(at = [var 1, var 2])
8604  point(at = [var 3, var 4])
8605  point(at = [var 5, var 6])
8606}
8607";
8608
8609        let program = Program::parse(initial_source).unwrap().0.unwrap();
8610
8611        let mut frontend = FrontendState::new();
8612
8613        let mock_ctx = ExecutorContext::new_mock(None).await;
8614        let version = Version(0);
8615
8616        frontend.program = program.clone();
8617        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8618        frontend.update_state_after_exec(outcome, true);
8619        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8620        let sketch_id = sketch_object.id;
8621        let sketch = expect_sketch(sketch_object);
8622        let segments = sketch
8623            .segments
8624            .iter()
8625            .take(3)
8626            .copied()
8627            .map(Into::into)
8628            .collect::<Vec<ConstraintSegment>>();
8629
8630        let constraint = Constraint::Coincident(Coincident {
8631            segments: segments.clone(),
8632        });
8633        let (src_delta, scene_delta) = frontend
8634            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8635            .await
8636            .unwrap();
8637        assert_eq!(
8638            src_delta.text.as_str(),
8639            "\
8640sketch(on = XY) {
8641  point1 = point(at = [var 1, var 2])
8642  point2 = point(at = [var 3, var 4])
8643  point3 = point(at = [var 5, var 6])
8644  coincident([point1, point2, point3])
8645}
8646"
8647        );
8648
8649        let constraint_object = scene_delta
8650            .new_graph
8651            .objects
8652            .iter()
8653            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8654            .unwrap();
8655
8656        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8657            panic!("expected a constraint object");
8658        };
8659
8660        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8661
8662        mock_ctx.close().await;
8663    }
8664
8665    #[tokio::test(flavor = "multi_thread")]
8666    async fn test_source_with_three_point_coincident_tracks_all_segments() {
8667        let initial_source = "\
8668sketch(on = XY) {
8669  point1 = point(at = [var 1, var 2])
8670  point2 = point(at = [var 3, var 4])
8671  point3 = point(at = [var 5, var 6])
8672  coincident([point1, point2, point3])
8673}
8674";
8675
8676        let program = Program::parse(initial_source).unwrap().0.unwrap();
8677
8678        let mut frontend = FrontendState::new();
8679
8680        let ctx = ExecutorContext::new_mock(None).await;
8681        frontend.program = program.clone();
8682        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8683        frontend.update_state_after_exec(outcome, true);
8684
8685        let constraint_object = frontend
8686            .scene_graph
8687            .objects
8688            .iter()
8689            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8690            .unwrap();
8691        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8692            panic!("expected a constraint object");
8693        };
8694
8695        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8696        let sketch = expect_sketch(sketch_object);
8697        let expected_segments = sketch
8698            .segments
8699            .iter()
8700            .take(3)
8701            .copied()
8702            .map(Into::into)
8703            .collect::<Vec<ConstraintSegment>>();
8704
8705        assert_eq!(
8706            constraint,
8707            &Constraint::Coincident(Coincident {
8708                segments: expected_segments,
8709            })
8710        );
8711
8712        ctx.close().await;
8713    }
8714
8715    #[tokio::test(flavor = "multi_thread")]
8716    async fn test_point_origin_coincident_preserves_order() {
8717        let initial_source = "\
8718sketch(on = XY) {
8719  point(at = [var 1, var 2])
8720}
8721";
8722
8723        for (origin_first, expected_source) in [
8724            (
8725                true,
8726                "\
8727sketch(on = XY) {
8728  point1 = point(at = [var 1, var 2])
8729  coincident([ORIGIN, point1])
8730}
8731",
8732            ),
8733            (
8734                false,
8735                "\
8736sketch(on = XY) {
8737  point1 = point(at = [var 1, var 2])
8738  coincident([point1, ORIGIN])
8739}
8740",
8741            ),
8742        ] {
8743            let program = Program::parse(initial_source).unwrap().0.unwrap();
8744
8745            let mut frontend = FrontendState::new();
8746
8747            let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8748            let mock_ctx = ExecutorContext::new_mock(None).await;
8749            let version = Version(0);
8750
8751            frontend.hack_set_program(&ctx, program).await.unwrap();
8752            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8753            let sketch_id = sketch_object.id;
8754            let sketch = expect_sketch(sketch_object);
8755            let point_id = *sketch.segments.first().unwrap();
8756
8757            let segments = if origin_first {
8758                vec![ConstraintSegment::ORIGIN, point_id.into()]
8759            } else {
8760                vec![point_id.into(), ConstraintSegment::ORIGIN]
8761            };
8762            let constraint = Constraint::Coincident(Coincident {
8763                segments: segments.clone(),
8764            });
8765            let (src_delta, scene_delta) = frontend
8766                .add_constraint(&mock_ctx, version, sketch_id, constraint)
8767                .await
8768                .unwrap();
8769            assert_eq!(src_delta.text.as_str(), expected_source);
8770
8771            let constraint_object = scene_delta
8772                .new_graph
8773                .objects
8774                .iter()
8775                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
8776                .unwrap();
8777
8778            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
8779                panic!("expected a constraint object");
8780            };
8781
8782            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
8783
8784            ctx.close().await;
8785            mock_ctx.close().await;
8786        }
8787    }
8788
8789    #[tokio::test(flavor = "multi_thread")]
8790    async fn test_coincident_of_line_end_points() {
8791        let initial_source = "\
8792sketch(on = XY) {
8793  line(start = [var 1, var 2], end = [var 3, var 4])
8794  line(start = [var 5, var 6], end = [var 7, var 8])
8795}
8796";
8797
8798        let program = Program::parse(initial_source).unwrap().0.unwrap();
8799
8800        let mut frontend = FrontendState::new();
8801
8802        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8803        let mock_ctx = ExecutorContext::new_mock(None).await;
8804        let version = Version(0);
8805
8806        frontend.hack_set_program(&ctx, program).await.unwrap();
8807        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8808        let sketch_id = sketch_object.id;
8809        let sketch = expect_sketch(sketch_object);
8810        let point0_id = *sketch.segments.get(1).unwrap();
8811        let point1_id = *sketch.segments.get(3).unwrap();
8812
8813        let constraint = Constraint::Coincident(Coincident {
8814            segments: vec![point0_id.into(), point1_id.into()],
8815        });
8816        let (src_delta, scene_delta) = frontend
8817            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8818            .await
8819            .unwrap();
8820        assert_eq!(
8821            src_delta.text.as_str(),
8822            "\
8823sketch(on = XY) {
8824  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
8825  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8826  coincident([line1.end, line2.start])
8827}
8828"
8829        );
8830        assert_eq!(
8831            scene_delta.new_graph.objects.len(),
8832            9,
8833            "{:#?}",
8834            scene_delta.new_graph.objects
8835        );
8836
8837        ctx.close().await;
8838        mock_ctx.close().await;
8839    }
8840
8841    #[tokio::test(flavor = "multi_thread")]
8842    async fn test_coincident_of_line_point_and_circle_segment() {
8843        let initial_source = "\
8844sketch(on = XY) {
8845  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8846  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8847}
8848";
8849        let program = Program::parse(initial_source).unwrap().0.unwrap();
8850        let mut frontend = FrontendState::new();
8851
8852        let mock_ctx = ExecutorContext::new_mock(None).await;
8853        let version = Version(0);
8854
8855        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
8856        frontend.program = program;
8857        frontend.update_state_after_exec(outcome, true);
8858        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
8859        let sketch_id = sketch_object.id;
8860        let sketch = expect_sketch(sketch_object);
8861
8862        let circle_id = sketch
8863            .segments
8864            .iter()
8865            .copied()
8866            .find(|seg_id| {
8867                matches!(
8868                    &frontend.scene_graph.objects[seg_id.0].kind,
8869                    ObjectKind::Segment {
8870                        segment: Segment::Circle(_)
8871                    }
8872                )
8873            })
8874            .expect("Expected a circle segment in sketch");
8875        let line_id = sketch
8876            .segments
8877            .iter()
8878            .copied()
8879            .find(|seg_id| {
8880                matches!(
8881                    &frontend.scene_graph.objects[seg_id.0].kind,
8882                    ObjectKind::Segment {
8883                        segment: Segment::Line(_)
8884                    }
8885                )
8886            })
8887            .expect("Expected a line segment in sketch");
8888
8889        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
8890            ObjectKind::Segment {
8891                segment: Segment::Line(line),
8892            } => line.start,
8893            _ => panic!("Expected line segment object"),
8894        };
8895
8896        let constraint = Constraint::Coincident(Coincident {
8897            segments: vec![line_start_point_id.into(), circle_id.into()],
8898        });
8899        let (src_delta, _scene_delta) = frontend
8900            .add_constraint(&mock_ctx, version, sketch_id, constraint)
8901            .await
8902            .unwrap();
8903        assert_eq!(
8904            src_delta.text.as_str(),
8905            "\
8906sketch(on = XY) {
8907  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8908  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
8909  coincident([line1.start, circle1])
8910}
8911"
8912        );
8913
8914        mock_ctx.close().await;
8915    }
8916
8917    #[tokio::test(flavor = "multi_thread")]
8918    async fn test_invalid_coincident_arc_and_line_preserves_state() {
8919        // Test that attempting an invalid coincident constraint (arc and line)
8920        // doesn't corrupt the state, allowing subsequent operations to work.
8921        // This test verifies the transactional fix in add_constraint that prevents
8922        // state corruption when invalid constraints are attempted.
8923        // Example: coincident constraint between an arc segment and a straight line segment
8924        // is geometrically invalid and should fail, but state should remain intact.
8925        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
8926        let program = Program::empty();
8927
8928        let mut frontend = FrontendState::new();
8929        frontend.program = program;
8930
8931        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8932        let mock_ctx = ExecutorContext::new_mock(None).await;
8933        let version = Version(0);
8934
8935        let sketch_args = SketchCtor {
8936            on: Plane::Default(PlaneName::Xy),
8937        };
8938        let (_src_delta, _scene_delta, sketch_id) = frontend
8939            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8940            .await
8941            .unwrap();
8942
8943        // Add an arc segment
8944        let arc_ctor = ArcCtor {
8945            start: Point2d {
8946                x: Expr::Var(Number {
8947                    value: 0.0,
8948                    units: NumericSuffix::Mm,
8949                }),
8950                y: Expr::Var(Number {
8951                    value: 0.0,
8952                    units: NumericSuffix::Mm,
8953                }),
8954            },
8955            end: Point2d {
8956                x: Expr::Var(Number {
8957                    value: 10.0,
8958                    units: NumericSuffix::Mm,
8959                }),
8960                y: Expr::Var(Number {
8961                    value: 10.0,
8962                    units: NumericSuffix::Mm,
8963                }),
8964            },
8965            center: Point2d {
8966                x: Expr::Var(Number {
8967                    value: 10.0,
8968                    units: NumericSuffix::Mm,
8969                }),
8970                y: Expr::Var(Number {
8971                    value: 0.0,
8972                    units: NumericSuffix::Mm,
8973                }),
8974            },
8975            construction: None,
8976        };
8977        let (_src_delta, scene_delta) = frontend
8978            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
8979            .await
8980            .unwrap();
8981        // The arc is the last object in new_objects (after the 3 points: start, end, center)
8982        let arc_id = *scene_delta.new_objects.last().unwrap();
8983
8984        // Add a line segment
8985        let line_ctor = LineCtor {
8986            start: Point2d {
8987                x: Expr::Var(Number {
8988                    value: 20.0,
8989                    units: NumericSuffix::Mm,
8990                }),
8991                y: Expr::Var(Number {
8992                    value: 0.0,
8993                    units: NumericSuffix::Mm,
8994                }),
8995            },
8996            end: Point2d {
8997                x: Expr::Var(Number {
8998                    value: 30.0,
8999                    units: NumericSuffix::Mm,
9000                }),
9001                y: Expr::Var(Number {
9002                    value: 10.0,
9003                    units: NumericSuffix::Mm,
9004                }),
9005            },
9006            construction: None,
9007        };
9008        let (_src_delta, scene_delta) = frontend
9009            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
9010            .await
9011            .unwrap();
9012        // The line is the last object in new_objects (after the 2 points: start, end)
9013        let line_id = *scene_delta.new_objects.last().unwrap();
9014
9015        // Attempt to add an invalid coincident constraint between arc and line
9016        // This should fail during execution, but state should remain intact
9017        let constraint = Constraint::Coincident(Coincident {
9018            segments: vec![arc_id.into(), line_id.into()],
9019        });
9020        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
9021
9022        // The constraint addition should fail (invalid constraint)
9023        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
9024
9025        // Verify state is not corrupted by checking that we can still access the scene graph
9026        // and that the original segments are still present with their source ranges
9027        let sketch_object_after =
9028            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
9029        let sketch_after = expect_sketch(sketch_object_after);
9030
9031        // Verify both segments are still in the sketch
9032        assert!(
9033            sketch_after.segments.contains(&arc_id),
9034            "Arc segment should still exist after failed constraint"
9035        );
9036        assert!(
9037            sketch_after.segments.contains(&line_id),
9038            "Line segment should still exist after failed constraint"
9039        );
9040
9041        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
9042        let arc_obj = frontend
9043            .scene_graph
9044            .objects
9045            .get(arc_id.0)
9046            .expect("Arc object should still be accessible");
9047        let line_obj = frontend
9048            .scene_graph
9049            .objects
9050            .get(line_id.0)
9051            .expect("Line object should still be accessible");
9052
9053        // Verify source ranges are still valid (not corrupted)
9054        // Just verify that the objects are still accessible and have the expected types
9055        match &arc_obj.kind {
9056            ObjectKind::Segment {
9057                segment: Segment::Arc(_),
9058            } => {}
9059            _ => panic!("Arc object should still be an arc segment"),
9060        }
9061        match &line_obj.kind {
9062            ObjectKind::Segment {
9063                segment: Segment::Line(_),
9064            } => {}
9065            _ => panic!("Line object should still be a line segment"),
9066        }
9067
9068        ctx.close().await;
9069        mock_ctx.close().await;
9070    }
9071
9072    #[tokio::test(flavor = "multi_thread")]
9073    async fn test_distance_two_points() {
9074        let initial_source = "\
9075sketch(on = XY) {
9076  point(at = [var 1, var 2])
9077  point(at = [var 3, var 4])
9078}
9079";
9080
9081        let program = Program::parse(initial_source).unwrap().0.unwrap();
9082
9083        let mut frontend = FrontendState::new();
9084
9085        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9086        let mock_ctx = ExecutorContext::new_mock(None).await;
9087        let version = Version(0);
9088
9089        frontend.hack_set_program(&ctx, program).await.unwrap();
9090        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9091        let sketch_id = sketch_object.id;
9092        let sketch = expect_sketch(sketch_object);
9093        let point0_id = *sketch.segments.first().unwrap();
9094        let point1_id = *sketch.segments.get(1).unwrap();
9095
9096        let constraint = Constraint::Distance(Distance {
9097            points: vec![point0_id.into(), point1_id.into()],
9098            distance: Number {
9099                value: 2.0,
9100                units: NumericSuffix::Mm,
9101            },
9102            label_position: None,
9103            source: Default::default(),
9104        });
9105        let (src_delta, scene_delta) = frontend
9106            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9107            .await
9108            .unwrap();
9109        assert_eq!(
9110            src_delta.text.as_str(),
9111            // The lack indentation is a formatter bug.
9112            "\
9113sketch(on = XY) {
9114  point1 = point(at = [var 1, var 2])
9115  point2 = point(at = [var 3, var 4])
9116  distance([point1, point2]) == 2mm
9117}
9118"
9119        );
9120        assert_eq!(
9121            scene_delta.new_graph.objects.len(),
9122            5,
9123            "{:#?}",
9124            scene_delta.new_graph.objects
9125        );
9126
9127        ctx.close().await;
9128        mock_ctx.close().await;
9129    }
9130
9131    #[tokio::test(flavor = "multi_thread")]
9132    async fn test_distance_two_points_with_label() {
9133        let initial_source = "\
9134sketch(on = XY) {
9135  point(at = [var 1, var 2])
9136  point(at = [var 3, var 4])
9137}
9138";
9139
9140        let program = Program::parse(initial_source).unwrap().0.unwrap();
9141
9142        let mut frontend = FrontendState::new();
9143
9144        let mock_ctx = ExecutorContext::new_mock(None).await;
9145        let version = Version(0);
9146
9147        frontend.program = program.clone();
9148        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9149        frontend.update_state_after_exec(outcome, true);
9150        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9151        let sketch_id = sketch_object.id;
9152        let sketch = expect_sketch(sketch_object);
9153        let point0_id = *sketch.segments.first().unwrap();
9154        let point1_id = *sketch.segments.get(1).unwrap();
9155
9156        let label_position = Point2d {
9157            x: Number {
9158                value: 10.0,
9159                units: NumericSuffix::Mm,
9160            },
9161            y: Number {
9162                value: 11.0,
9163                units: NumericSuffix::Mm,
9164            },
9165        };
9166        let constraint = Constraint::Distance(Distance {
9167            points: vec![point0_id.into(), point1_id.into()],
9168            distance: Number {
9169                value: 2.0,
9170                units: NumericSuffix::Mm,
9171            },
9172            label_position: Some(label_position.clone()),
9173            source: Default::default(),
9174        });
9175        let (src_delta, scene_delta) = frontend
9176            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9177            .await
9178            .unwrap();
9179        assert_eq!(
9180            src_delta.text.as_str(),
9181            "\
9182sketch(on = XY) {
9183  point1 = point(at = [var 1, var 2])
9184  point2 = point(at = [var 3, var 4])
9185  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9186}
9187"
9188        );
9189
9190        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9191        let sketch = expect_sketch(sketch_object);
9192        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9193        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9194            panic!("Expected constraint object");
9195        };
9196        let Constraint::Distance(distance) = constraint else {
9197            panic!("Expected distance constraint");
9198        };
9199        assert_eq!(distance.label_position, Some(label_position));
9200
9201        mock_ctx.close().await;
9202    }
9203
9204    #[tokio::test(flavor = "multi_thread")]
9205    async fn test_edit_distance_constraint_label_position() {
9206        let initial_source = "\
9207sketch(on = XY) {
9208  point(at = [var 1, var 2])
9209  point(at = [var 3, var 2])
9210}
9211";
9212
9213        let program = Program::parse(initial_source).unwrap().0.unwrap();
9214
9215        let mut frontend = FrontendState::new();
9216
9217        let mock_ctx = ExecutorContext::new_mock(None).await;
9218        let version = Version(0);
9219
9220        frontend.program = program.clone();
9221        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9222        frontend.update_state_after_exec(outcome, true);
9223        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9224        let sketch_id = sketch_object.id;
9225        let sketch = expect_sketch(sketch_object);
9226        let point0_id = *sketch.segments.first().unwrap();
9227        let point1_id = *sketch.segments.get(1).unwrap();
9228
9229        let constraint = Constraint::Distance(Distance {
9230            points: vec![point0_id.into(), point1_id.into()],
9231            distance: Number {
9232                value: 2.0,
9233                units: NumericSuffix::Mm,
9234            },
9235            label_position: None,
9236            source: Default::default(),
9237        });
9238        let (_, scene_delta) = frontend
9239            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9240            .await
9241            .unwrap();
9242        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9243        let sketch = expect_sketch(sketch_object);
9244        let constraint_id = sketch.constraints[0];
9245        let label_position = Point2d {
9246            x: Number {
9247                value: 10.0,
9248                units: NumericSuffix::Mm,
9249            },
9250            y: Number {
9251                value: 11.0,
9252                units: NumericSuffix::Mm,
9253            },
9254        };
9255
9256        let (src_delta, scene_delta) = frontend
9257            .edit_distance_constraint_label_position(
9258                &mock_ctx,
9259                version,
9260                sketch_id,
9261                constraint_id,
9262                label_position.clone(),
9263                vec![],
9264            )
9265            .await
9266            .unwrap();
9267        assert_eq!(
9268            src_delta.text.as_str(),
9269            "\
9270sketch(on = XY) {
9271  point1 = point(at = [var 1mm, var 2mm])
9272  point2 = point(at = [var 3mm, var 2mm])
9273  distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9274}
9275"
9276        );
9277
9278        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9279        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9280            panic!("Expected constraint object");
9281        };
9282        let Constraint::Distance(distance) = constraint else {
9283            panic!("Expected distance constraint");
9284        };
9285        assert_eq!(distance.label_position, Some(label_position));
9286
9287        mock_ctx.close().await;
9288    }
9289
9290    #[tokio::test(flavor = "multi_thread")]
9291    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
9292        let initial_source = "\
9293sketch(on = XY) {
9294  point1 = point(at = [var 0mm, var 0mm])
9295  point2 = point(at = [var 10mm, var 0mm])
9296  distance([point1, point2]) == 5mm
9297}
9298";
9299
9300        let program = Program::parse(initial_source).unwrap().0.unwrap();
9301        let mut frontend = FrontendState::new();
9302        let mock_ctx = ExecutorContext::new_mock(None).await;
9303        let version = Version(0);
9304
9305        frontend.program = program.clone();
9306        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9307        frontend.update_state_after_exec(outcome, true);
9308        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9309        let sketch_id = sketch_object.id;
9310        let sketch = expect_sketch(sketch_object);
9311        let point0_id = sketch.segments[0];
9312        let point1_id = sketch.segments[1];
9313        let constraint_id = sketch.constraints[0];
9314
9315        let edited_segments = vec![ExistingSegmentCtor {
9316            id: point0_id,
9317            ctor: SegmentCtor::Point(PointCtor {
9318                position: Point2d {
9319                    x: Expr::Var(Number {
9320                        value: 2.0,
9321                        units: NumericSuffix::Mm,
9322                    }),
9323                    y: Expr::Var(Number {
9324                        value: 1.0,
9325                        units: NumericSuffix::Mm,
9326                    }),
9327                },
9328            }),
9329        }];
9330        let (_, scene_delta) = frontend
9331            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
9332            .await
9333            .unwrap();
9334        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
9335        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
9336
9337        let label_position = Point2d {
9338            x: Number {
9339                value: 3.0,
9340                units: NumericSuffix::Mm,
9341            },
9342            y: Number {
9343                value: 4.0,
9344                units: NumericSuffix::Mm,
9345            },
9346        };
9347        let (_, scene_delta) = frontend
9348            .edit_distance_constraint_label_position(
9349                &mock_ctx,
9350                version,
9351                sketch_id,
9352                constraint_id,
9353                label_position,
9354                vec![point0_id],
9355            )
9356            .await
9357            .unwrap();
9358
9359        assert_point_position_close(
9360            point_position(&scene_delta.new_graph, point0_id),
9361            point0_after_segment_edit,
9362        );
9363        assert_point_position_close(
9364            point_position(&scene_delta.new_graph, point1_id),
9365            point1_after_segment_edit,
9366        );
9367
9368        mock_ctx.close().await;
9369    }
9370
9371    #[tokio::test(flavor = "multi_thread")]
9372    async fn test_horizontal_distance_two_points() {
9373        let initial_source = "\
9374sketch(on = XY) {
9375  point(at = [var 1, var 2])
9376  point(at = [var 3, var 4])
9377}
9378";
9379
9380        let program = Program::parse(initial_source).unwrap().0.unwrap();
9381
9382        let mut frontend = FrontendState::new();
9383
9384        let mock_ctx = ExecutorContext::new_mock(None).await;
9385        let version = Version(0);
9386
9387        frontend.program = program.clone();
9388        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9389        frontend.update_state_after_exec(outcome, true);
9390        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9391        let sketch_id = sketch_object.id;
9392        let sketch = expect_sketch(sketch_object);
9393        let point0_id = *sketch.segments.first().unwrap();
9394        let point1_id = *sketch.segments.get(1).unwrap();
9395        let label_position = Point2d {
9396            x: Number {
9397                value: 10.0,
9398                units: NumericSuffix::Mm,
9399            },
9400            y: Number {
9401                value: 11.0,
9402                units: NumericSuffix::Mm,
9403            },
9404        };
9405
9406        let constraint = Constraint::HorizontalDistance(Distance {
9407            points: vec![point0_id.into(), point1_id.into()],
9408            distance: Number {
9409                value: 2.0,
9410                units: NumericSuffix::Mm,
9411            },
9412            label_position: Some(label_position.clone()),
9413            source: Default::default(),
9414        });
9415        let (src_delta, scene_delta) = frontend
9416            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9417            .await
9418            .unwrap();
9419        assert_eq!(
9420            src_delta.text.as_str(),
9421            // The lack indentation is a formatter bug.
9422            "\
9423sketch(on = XY) {
9424  point1 = point(at = [var 1, var 2])
9425  point2 = point(at = [var 3, var 4])
9426  horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9427}
9428"
9429        );
9430        assert_eq!(
9431            scene_delta.new_graph.objects.len(),
9432            5,
9433            "{:#?}",
9434            scene_delta.new_graph.objects
9435        );
9436        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9437        let sketch = expect_sketch(sketch_object);
9438        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9439        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9440            panic!("Expected constraint object");
9441        };
9442        let Constraint::HorizontalDistance(distance) = constraint else {
9443            panic!("Expected horizontal distance constraint");
9444        };
9445        assert_eq!(distance.label_position, Some(label_position));
9446
9447        mock_ctx.close().await;
9448    }
9449
9450    #[tokio::test(flavor = "multi_thread")]
9451    async fn test_radius_single_arc_segment() {
9452        let initial_source = "\
9453sketch(on = XY) {
9454  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9455}
9456";
9457
9458        let program = Program::parse(initial_source).unwrap().0.unwrap();
9459
9460        let mut frontend = FrontendState::new();
9461
9462        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9463        let mock_ctx = ExecutorContext::new_mock(None).await;
9464        let version = Version(0);
9465
9466        frontend.hack_set_program(&ctx, program).await.unwrap();
9467        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9468        let sketch_id = sketch_object.id;
9469        let sketch = expect_sketch(sketch_object);
9470        // Find the arc segment (not the points)
9471        let arc_id = sketch
9472            .segments
9473            .iter()
9474            .find(|&seg_id| {
9475                let obj = frontend.scene_graph.objects.get(seg_id.0);
9476                matches!(
9477                    obj.map(|o| &o.kind),
9478                    Some(ObjectKind::Segment {
9479                        segment: Segment::Arc(_)
9480                    })
9481                )
9482            })
9483            .unwrap();
9484
9485        let constraint = Constraint::Radius(Radius {
9486            arc: *arc_id,
9487            radius: Number {
9488                value: 5.0,
9489                units: NumericSuffix::Mm,
9490            },
9491            label_position: None,
9492            source: Default::default(),
9493        });
9494        let (src_delta, scene_delta) = frontend
9495            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9496            .await
9497            .unwrap();
9498        assert_eq!(
9499            src_delta.text.as_str(),
9500            // The lack indentation is a formatter bug.
9501            "\
9502sketch(on = XY) {
9503  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9504  radius(arc1) == 5mm
9505}
9506"
9507        );
9508        assert_eq!(
9509            scene_delta.new_graph.objects.len(),
9510            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
9511            "{:#?}",
9512            scene_delta.new_graph.objects
9513        );
9514
9515        ctx.close().await;
9516        mock_ctx.close().await;
9517    }
9518
9519    #[tokio::test(flavor = "multi_thread")]
9520    async fn test_radius_single_arc_segment_with_label_position() {
9521        let initial_source = "\
9522sketch(on = XY) {
9523  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9524}
9525";
9526
9527        let program = Program::parse(initial_source).unwrap().0.unwrap();
9528        let mut frontend = FrontendState::new();
9529        let mock_ctx = ExecutorContext::new_mock(None).await;
9530        let version = Version(0);
9531
9532        frontend.program = program.clone();
9533        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9534        frontend.update_state_after_exec(outcome, true);
9535        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9536        let sketch_id = sketch_object.id;
9537        let sketch = expect_sketch(sketch_object);
9538        let arc_id = sketch
9539            .segments
9540            .iter()
9541            .find(|&seg_id| {
9542                let obj = frontend.scene_graph.objects.get(seg_id.0);
9543                matches!(
9544                    obj.map(|o| &o.kind),
9545                    Some(ObjectKind::Segment {
9546                        segment: Segment::Arc(_)
9547                    })
9548                )
9549            })
9550            .unwrap();
9551
9552        let label_position = Point2d {
9553            x: Number {
9554                value: 10.0,
9555                units: NumericSuffix::Mm,
9556            },
9557            y: Number {
9558                value: 11.0,
9559                units: NumericSuffix::Mm,
9560            },
9561        };
9562        let constraint = Constraint::Radius(Radius {
9563            arc: *arc_id,
9564            radius: Number {
9565                value: 5.0,
9566                units: NumericSuffix::Mm,
9567            },
9568            label_position: Some(label_position.clone()),
9569            source: Default::default(),
9570        });
9571        let (src_delta, scene_delta) = frontend
9572            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9573            .await
9574            .unwrap();
9575        assert_eq!(
9576            src_delta.text.as_str(),
9577            "\
9578sketch(on = XY) {
9579  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
9580  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9581}
9582"
9583        );
9584
9585        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9586        let sketch = expect_sketch(sketch_object);
9587        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9588        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9589            panic!("Expected constraint object");
9590        };
9591        let Constraint::Radius(radius) = constraint else {
9592            panic!("Expected radius constraint");
9593        };
9594        assert_eq!(radius.label_position, Some(label_position));
9595
9596        mock_ctx.close().await;
9597    }
9598
9599    #[tokio::test(flavor = "multi_thread")]
9600    async fn test_edit_radius_constraint_label_position() {
9601        let initial_source = "\
9602sketch(on = XY) {
9603  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9604  radius(arc1) == 5mm
9605}
9606";
9607
9608        let program = Program::parse(initial_source).unwrap().0.unwrap();
9609        let mut frontend = FrontendState::new();
9610        let mock_ctx = ExecutorContext::new_mock(None).await;
9611        let version = Version(0);
9612
9613        frontend.program = program.clone();
9614        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9615        frontend.update_state_after_exec(outcome, true);
9616        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9617        let sketch_id = sketch_object.id;
9618        let sketch = expect_sketch(sketch_object);
9619        let constraint_id = sketch.constraints[0];
9620        let label_position = Point2d {
9621            x: Number {
9622                value: 10.0,
9623                units: NumericSuffix::Mm,
9624            },
9625            y: Number {
9626                value: 11.0,
9627                units: NumericSuffix::Mm,
9628            },
9629        };
9630
9631        let (src_delta, scene_delta) = frontend
9632            .edit_distance_constraint_label_position(
9633                &mock_ctx,
9634                version,
9635                sketch_id,
9636                constraint_id,
9637                label_position.clone(),
9638                vec![],
9639            )
9640            .await
9641            .unwrap();
9642        assert_eq!(
9643            src_delta.text.as_str(),
9644            "\
9645sketch(on = XY) {
9646  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
9647  radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
9648}
9649"
9650        );
9651
9652        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
9653        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9654            panic!("Expected constraint object");
9655        };
9656        let Constraint::Radius(radius) = constraint else {
9657            panic!("Expected radius constraint");
9658        };
9659        assert_eq!(radius.label_position, Some(label_position));
9660
9661        mock_ctx.close().await;
9662    }
9663
9664    #[tokio::test(flavor = "multi_thread")]
9665    async fn test_vertical_distance_two_points() {
9666        let initial_source = "\
9667sketch(on = XY) {
9668  point(at = [var 1, var 2])
9669  point(at = [var 3, var 4])
9670}
9671";
9672
9673        let program = Program::parse(initial_source).unwrap().0.unwrap();
9674
9675        let mut frontend = FrontendState::new();
9676
9677        let mock_ctx = ExecutorContext::new_mock(None).await;
9678        let version = Version(0);
9679
9680        frontend.program = program.clone();
9681        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9682        frontend.update_state_after_exec(outcome, true);
9683        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9684        let sketch_id = sketch_object.id;
9685        let sketch = expect_sketch(sketch_object);
9686        let point0_id = *sketch.segments.first().unwrap();
9687        let point1_id = *sketch.segments.get(1).unwrap();
9688        let label_position = Point2d {
9689            x: Number {
9690                value: 10.0,
9691                units: NumericSuffix::Mm,
9692            },
9693            y: Number {
9694                value: 11.0,
9695                units: NumericSuffix::Mm,
9696            },
9697        };
9698
9699        let constraint = Constraint::VerticalDistance(Distance {
9700            points: vec![point0_id.into(), point1_id.into()],
9701            distance: Number {
9702                value: 2.0,
9703                units: NumericSuffix::Mm,
9704            },
9705            label_position: Some(label_position.clone()),
9706            source: Default::default(),
9707        });
9708        let (src_delta, scene_delta) = frontend
9709            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9710            .await
9711            .unwrap();
9712        assert_eq!(
9713            src_delta.text.as_str(),
9714            // The lack indentation is a formatter bug.
9715            "\
9716sketch(on = XY) {
9717  point1 = point(at = [var 1, var 2])
9718  point2 = point(at = [var 3, var 4])
9719  verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
9720}
9721"
9722        );
9723        assert_eq!(
9724            scene_delta.new_graph.objects.len(),
9725            5,
9726            "{:#?}",
9727            scene_delta.new_graph.objects
9728        );
9729        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9730        let sketch = expect_sketch(sketch_object);
9731        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9732        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9733            panic!("Expected constraint object");
9734        };
9735        let Constraint::VerticalDistance(distance) = constraint else {
9736            panic!("Expected vertical distance constraint");
9737        };
9738        assert_eq!(distance.label_position, Some(label_position));
9739
9740        mock_ctx.close().await;
9741    }
9742
9743    #[tokio::test(flavor = "multi_thread")]
9744    async fn test_add_fixed_standalone_point() {
9745        let initial_source = "\
9746sketch(on = XY) {
9747  point(at = [var 1, var 2])
9748}
9749";
9750
9751        let program = Program::parse(initial_source).unwrap().0.unwrap();
9752
9753        let mut frontend = FrontendState::new();
9754
9755        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9756        let mock_ctx = ExecutorContext::new_mock(None).await;
9757        let version = Version(0);
9758
9759        frontend.hack_set_program(&ctx, program).await.unwrap();
9760        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9761        let sketch_id = sketch_object.id;
9762        let sketch = expect_sketch(sketch_object);
9763        let point_id = *sketch.segments.first().unwrap();
9764
9765        let (src_delta, scene_delta) = frontend
9766            .add_constraint(
9767                &mock_ctx,
9768                version,
9769                sketch_id,
9770                Constraint::Fixed(Fixed {
9771                    points: vec![FixedPoint {
9772                        point: point_id,
9773                        position: Point2d {
9774                            x: Number {
9775                                value: 2.0,
9776                                units: NumericSuffix::Mm,
9777                            },
9778                            y: Number {
9779                                value: 3.0,
9780                                units: NumericSuffix::Mm,
9781                            },
9782                        },
9783                    }],
9784                }),
9785            )
9786            .await
9787            .unwrap();
9788        assert_eq!(
9789            src_delta.text.as_str(),
9790            "\
9791sketch(on = XY) {
9792  point1 = point(at = [var 1, var 2])
9793  fixed([point1, [2mm, 3mm]])
9794}
9795"
9796        );
9797        assert_eq!(
9798            scene_delta.new_graph.objects.len(),
9799            4,
9800            "{:#?}",
9801            scene_delta.new_graph.objects
9802        );
9803
9804        ctx.close().await;
9805        mock_ctx.close().await;
9806    }
9807
9808    #[tokio::test(flavor = "multi_thread")]
9809    async fn test_add_fixed_multiple_points() {
9810        let initial_source = "\
9811sketch(on = XY) {
9812  point(at = [var 1, var 2])
9813  point(at = [var 3, var 4])
9814}
9815";
9816
9817        let program = Program::parse(initial_source).unwrap().0.unwrap();
9818
9819        let mut frontend = FrontendState::new();
9820
9821        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9822        let mock_ctx = ExecutorContext::new_mock(None).await;
9823        let version = Version(0);
9824
9825        frontend.hack_set_program(&ctx, program).await.unwrap();
9826        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9827        let sketch_id = sketch_object.id;
9828        let sketch = expect_sketch(sketch_object);
9829        let point0_id = *sketch.segments.first().unwrap();
9830        let point1_id = *sketch.segments.get(1).unwrap();
9831
9832        let (src_delta, scene_delta) = frontend
9833            .add_constraint(
9834                &mock_ctx,
9835                version,
9836                sketch_id,
9837                Constraint::Fixed(Fixed {
9838                    points: vec![
9839                        FixedPoint {
9840                            point: point0_id,
9841                            position: Point2d {
9842                                x: Number {
9843                                    value: 2.0,
9844                                    units: NumericSuffix::Mm,
9845                                },
9846                                y: Number {
9847                                    value: 3.0,
9848                                    units: NumericSuffix::Mm,
9849                                },
9850                            },
9851                        },
9852                        FixedPoint {
9853                            point: point1_id,
9854                            position: Point2d {
9855                                x: Number {
9856                                    value: 4.0,
9857                                    units: NumericSuffix::Mm,
9858                                },
9859                                y: Number {
9860                                    value: 5.0,
9861                                    units: NumericSuffix::Mm,
9862                                },
9863                            },
9864                        },
9865                    ],
9866                }),
9867            )
9868            .await
9869            .unwrap();
9870        assert_eq!(
9871            src_delta.text.as_str(),
9872            "\
9873sketch(on = XY) {
9874  point1 = point(at = [var 1, var 2])
9875  point2 = point(at = [var 3, var 4])
9876  fixed([point1, [2mm, 3mm]])
9877  fixed([point2, [4mm, 5mm]])
9878}
9879"
9880        );
9881        assert_eq!(
9882            scene_delta.new_graph.objects.len(),
9883            6,
9884            "{:#?}",
9885            scene_delta.new_graph.objects
9886        );
9887
9888        ctx.close().await;
9889        mock_ctx.close().await;
9890    }
9891
9892    #[tokio::test(flavor = "multi_thread")]
9893    async fn test_add_fixed_owned_point() {
9894        let initial_source = "\
9895sketch(on = XY) {
9896  line(start = [var 1, var 2], end = [var 3, var 4])
9897}
9898";
9899
9900        let program = Program::parse(initial_source).unwrap().0.unwrap();
9901
9902        let mut frontend = FrontendState::new();
9903
9904        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9905        let mock_ctx = ExecutorContext::new_mock(None).await;
9906        let version = Version(0);
9907
9908        frontend.hack_set_program(&ctx, program).await.unwrap();
9909        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9910        let sketch_id = sketch_object.id;
9911        let sketch = expect_sketch(sketch_object);
9912        let line_start_id = *sketch.segments.first().unwrap();
9913
9914        let (src_delta, scene_delta) = frontend
9915            .add_constraint(
9916                &mock_ctx,
9917                version,
9918                sketch_id,
9919                Constraint::Fixed(Fixed {
9920                    points: vec![FixedPoint {
9921                        point: line_start_id,
9922                        position: Point2d {
9923                            x: Number {
9924                                value: 2.0,
9925                                units: NumericSuffix::Mm,
9926                            },
9927                            y: Number {
9928                                value: 3.0,
9929                                units: NumericSuffix::Mm,
9930                            },
9931                        },
9932                    }],
9933                }),
9934            )
9935            .await
9936            .unwrap();
9937        assert_eq!(
9938            src_delta.text.as_str(),
9939            "\
9940sketch(on = XY) {
9941  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9942  fixed([line1.start, [2mm, 3mm]])
9943}
9944"
9945        );
9946        assert_eq!(
9947            scene_delta.new_graph.objects.len(),
9948            6,
9949            "{:#?}",
9950            scene_delta.new_graph.objects
9951        );
9952
9953        ctx.close().await;
9954        mock_ctx.close().await;
9955    }
9956
9957    #[tokio::test(flavor = "multi_thread")]
9958    async fn test_radius_error_cases() {
9959        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9960        let mock_ctx = ExecutorContext::new_mock(None).await;
9961        let version = Version(0);
9962
9963        // Test: Single point should error
9964        let initial_source_point = "\
9965sketch(on = XY) {
9966  point(at = [var 1, var 2])
9967}
9968";
9969        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
9970        let mut frontend_point = FrontendState::new();
9971        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
9972        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
9973        let sketch_id_point = sketch_object_point.id;
9974        let sketch_point = expect_sketch(sketch_object_point);
9975        let point_id = *sketch_point.segments.first().unwrap();
9976
9977        let constraint_point = Constraint::Radius(Radius {
9978            arc: point_id,
9979            radius: Number {
9980                value: 5.0,
9981                units: NumericSuffix::Mm,
9982            },
9983            label_position: None,
9984            source: Default::default(),
9985        });
9986        let result_point = frontend_point
9987            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
9988            .await;
9989        assert!(result_point.is_err(), "Single point should error for radius");
9990
9991        // Test: Single line segment should error (only arc segments supported)
9992        let initial_source_line = "\
9993sketch(on = XY) {
9994  line(start = [var 1, var 2], end = [var 3, var 4])
9995}
9996";
9997        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
9998        let mut frontend_line = FrontendState::new();
9999        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10000        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10001        let sketch_id_line = sketch_object_line.id;
10002        let sketch_line = expect_sketch(sketch_object_line);
10003        let line_id = *sketch_line.segments.first().unwrap();
10004
10005        let constraint_line = Constraint::Radius(Radius {
10006            arc: line_id,
10007            radius: Number {
10008                value: 5.0,
10009                units: NumericSuffix::Mm,
10010            },
10011            label_position: None,
10012            source: Default::default(),
10013        });
10014        let result_line = frontend_line
10015            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10016            .await;
10017        assert!(result_line.is_err(), "Single line segment should error for radius");
10018
10019        ctx.close().await;
10020        mock_ctx.close().await;
10021    }
10022
10023    #[tokio::test(flavor = "multi_thread")]
10024    async fn test_diameter_single_arc_segment() {
10025        let initial_source = "\
10026sketch(on = XY) {
10027  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10028}
10029";
10030
10031        let program = Program::parse(initial_source).unwrap().0.unwrap();
10032
10033        let mut frontend = FrontendState::new();
10034
10035        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10036        let mock_ctx = ExecutorContext::new_mock(None).await;
10037        let version = Version(0);
10038
10039        frontend.hack_set_program(&ctx, program).await.unwrap();
10040        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10041        let sketch_id = sketch_object.id;
10042        let sketch = expect_sketch(sketch_object);
10043        // Find the arc segment (not the points)
10044        let arc_id = sketch
10045            .segments
10046            .iter()
10047            .find(|&seg_id| {
10048                let obj = frontend.scene_graph.objects.get(seg_id.0);
10049                matches!(
10050                    obj.map(|o| &o.kind),
10051                    Some(ObjectKind::Segment {
10052                        segment: Segment::Arc(_)
10053                    })
10054                )
10055            })
10056            .unwrap();
10057
10058        let constraint = Constraint::Diameter(Diameter {
10059            arc: *arc_id,
10060            diameter: Number {
10061                value: 10.0,
10062                units: NumericSuffix::Mm,
10063            },
10064            label_position: None,
10065            source: Default::default(),
10066        });
10067        let (src_delta, scene_delta) = frontend
10068            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10069            .await
10070            .unwrap();
10071        assert_eq!(
10072            src_delta.text.as_str(),
10073            // The lack indentation is a formatter bug.
10074            "\
10075sketch(on = XY) {
10076  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10077  diameter(arc1) == 10mm
10078}
10079"
10080        );
10081        assert_eq!(
10082            scene_delta.new_graph.objects.len(),
10083            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
10084            "{:#?}",
10085            scene_delta.new_graph.objects
10086        );
10087
10088        ctx.close().await;
10089        mock_ctx.close().await;
10090    }
10091
10092    #[tokio::test(flavor = "multi_thread")]
10093    async fn test_diameter_single_arc_segment_with_label_position() {
10094        let initial_source = "\
10095sketch(on = XY) {
10096  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10097}
10098";
10099
10100        let program = Program::parse(initial_source).unwrap().0.unwrap();
10101        let mut frontend = FrontendState::new();
10102        let mock_ctx = ExecutorContext::new_mock(None).await;
10103        let version = Version(0);
10104
10105        frontend.program = program.clone();
10106        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10107        frontend.update_state_after_exec(outcome, true);
10108        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10109        let sketch_id = sketch_object.id;
10110        let sketch = expect_sketch(sketch_object);
10111        let arc_id = sketch
10112            .segments
10113            .iter()
10114            .find(|&seg_id| {
10115                let obj = frontend.scene_graph.objects.get(seg_id.0);
10116                matches!(
10117                    obj.map(|o| &o.kind),
10118                    Some(ObjectKind::Segment {
10119                        segment: Segment::Arc(_)
10120                    })
10121                )
10122            })
10123            .unwrap();
10124
10125        let label_position = Point2d {
10126            x: Number {
10127                value: 10.0,
10128                units: NumericSuffix::Mm,
10129            },
10130            y: Number {
10131                value: 11.0,
10132                units: NumericSuffix::Mm,
10133            },
10134        };
10135        let constraint = Constraint::Diameter(Diameter {
10136            arc: *arc_id,
10137            diameter: Number {
10138                value: 10.0,
10139                units: NumericSuffix::Mm,
10140            },
10141            label_position: Some(label_position.clone()),
10142            source: Default::default(),
10143        });
10144        let (src_delta, scene_delta) = frontend
10145            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10146            .await
10147            .unwrap();
10148        assert_eq!(
10149            src_delta.text.as_str(),
10150            "\
10151sketch(on = XY) {
10152  arc1 = arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
10153  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10154}
10155"
10156        );
10157
10158        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10159        let sketch = expect_sketch(sketch_object);
10160        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10161        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10162            panic!("Expected constraint object");
10163        };
10164        let Constraint::Diameter(diameter) = constraint else {
10165            panic!("Expected diameter constraint");
10166        };
10167        assert_eq!(diameter.label_position, Some(label_position));
10168
10169        mock_ctx.close().await;
10170    }
10171
10172    #[tokio::test(flavor = "multi_thread")]
10173    async fn test_edit_diameter_constraint_label_position() {
10174        let initial_source = "\
10175sketch(on = XY) {
10176  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10177  diameter(arc1) == 10mm
10178}
10179";
10180
10181        let program = Program::parse(initial_source).unwrap().0.unwrap();
10182        let mut frontend = FrontendState::new();
10183        let mock_ctx = ExecutorContext::new_mock(None).await;
10184        let version = Version(0);
10185
10186        frontend.program = program.clone();
10187        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10188        frontend.update_state_after_exec(outcome, true);
10189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10190        let sketch_id = sketch_object.id;
10191        let sketch = expect_sketch(sketch_object);
10192        let constraint_id = sketch.constraints[0];
10193        let label_position = Point2d {
10194            x: Number {
10195                value: 10.0,
10196                units: NumericSuffix::Mm,
10197            },
10198            y: Number {
10199                value: 11.0,
10200                units: NumericSuffix::Mm,
10201            },
10202        };
10203
10204        let (src_delta, scene_delta) = frontend
10205            .edit_distance_constraint_label_position(
10206                &mock_ctx,
10207                version,
10208                sketch_id,
10209                constraint_id,
10210                label_position.clone(),
10211                vec![],
10212            )
10213            .await
10214            .unwrap();
10215        assert_eq!(
10216            src_delta.text.as_str(),
10217            "\
10218sketch(on = XY) {
10219  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
10220  diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
10221}
10222"
10223        );
10224
10225        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10226        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10227            panic!("Expected constraint object");
10228        };
10229        let Constraint::Diameter(diameter) = constraint else {
10230            panic!("Expected diameter constraint");
10231        };
10232        assert_eq!(diameter.label_position, Some(label_position));
10233
10234        mock_ctx.close().await;
10235    }
10236
10237    #[tokio::test(flavor = "multi_thread")]
10238    async fn test_diameter_error_cases() {
10239        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10240        let mock_ctx = ExecutorContext::new_mock(None).await;
10241        let version = Version(0);
10242
10243        // Test: Single point should error
10244        let initial_source_point = "\
10245sketch(on = XY) {
10246  point(at = [var 1, var 2])
10247}
10248";
10249        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
10250        let mut frontend_point = FrontendState::new();
10251        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
10252        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
10253        let sketch_id_point = sketch_object_point.id;
10254        let sketch_point = expect_sketch(sketch_object_point);
10255        let point_id = *sketch_point.segments.first().unwrap();
10256
10257        let constraint_point = Constraint::Diameter(Diameter {
10258            arc: point_id,
10259            diameter: Number {
10260                value: 10.0,
10261                units: NumericSuffix::Mm,
10262            },
10263            label_position: None,
10264            source: Default::default(),
10265        });
10266        let result_point = frontend_point
10267            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
10268            .await;
10269        assert!(result_point.is_err(), "Single point should error for diameter");
10270
10271        // Test: Single line segment should error (only arc segments supported)
10272        let initial_source_line = "\
10273sketch(on = XY) {
10274  line(start = [var 1, var 2], end = [var 3, var 4])
10275}
10276";
10277        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
10278        let mut frontend_line = FrontendState::new();
10279        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
10280        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
10281        let sketch_id_line = sketch_object_line.id;
10282        let sketch_line = expect_sketch(sketch_object_line);
10283        let line_id = *sketch_line.segments.first().unwrap();
10284
10285        let constraint_line = Constraint::Diameter(Diameter {
10286            arc: line_id,
10287            diameter: Number {
10288                value: 10.0,
10289                units: NumericSuffix::Mm,
10290            },
10291            label_position: None,
10292            source: Default::default(),
10293        });
10294        let result_line = frontend_line
10295            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
10296            .await;
10297        assert!(result_line.is_err(), "Single line segment should error for diameter");
10298
10299        ctx.close().await;
10300        mock_ctx.close().await;
10301    }
10302
10303    #[tokio::test(flavor = "multi_thread")]
10304    async fn test_line_horizontal() {
10305        let initial_source = "\
10306sketch(on = XY) {
10307  line(start = [var 1, var 2], end = [var 3, var 4])
10308}
10309";
10310
10311        let program = Program::parse(initial_source).unwrap().0.unwrap();
10312
10313        let mut frontend = FrontendState::new();
10314
10315        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10316        let mock_ctx = ExecutorContext::new_mock(None).await;
10317        let version = Version(0);
10318
10319        frontend.hack_set_program(&ctx, program).await.unwrap();
10320        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10321        let sketch_id = sketch_object.id;
10322        let sketch = expect_sketch(sketch_object);
10323        let line1_id = *sketch.segments.get(2).unwrap();
10324
10325        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
10326        let (src_delta, scene_delta) = frontend
10327            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10328            .await
10329            .unwrap();
10330        assert_eq!(
10331            src_delta.text.as_str(),
10332            "\
10333sketch(on = XY) {
10334  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10335  horizontal(line1)
10336}
10337"
10338        );
10339        assert_eq!(
10340            scene_delta.new_graph.objects.len(),
10341            6,
10342            "{:#?}",
10343            scene_delta.new_graph.objects
10344        );
10345
10346        ctx.close().await;
10347        mock_ctx.close().await;
10348    }
10349
10350    #[tokio::test(flavor = "multi_thread")]
10351    async fn test_line_vertical() {
10352        let initial_source = "\
10353sketch(on = XY) {
10354  line(start = [var 1, var 2], end = [var 3, var 4])
10355}
10356";
10357
10358        let program = Program::parse(initial_source).unwrap().0.unwrap();
10359
10360        let mut frontend = FrontendState::new();
10361
10362        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10363        let mock_ctx = ExecutorContext::new_mock(None).await;
10364        let version = Version(0);
10365
10366        frontend.hack_set_program(&ctx, program).await.unwrap();
10367        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10368        let sketch_id = sketch_object.id;
10369        let sketch = expect_sketch(sketch_object);
10370        let line1_id = *sketch.segments.get(2).unwrap();
10371
10372        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
10373        let (src_delta, scene_delta) = frontend
10374            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10375            .await
10376            .unwrap();
10377        assert_eq!(
10378            src_delta.text.as_str(),
10379            "\
10380sketch(on = XY) {
10381  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10382  vertical(line1)
10383}
10384"
10385        );
10386        assert_eq!(
10387            scene_delta.new_graph.objects.len(),
10388            6,
10389            "{:#?}",
10390            scene_delta.new_graph.objects
10391        );
10392
10393        ctx.close().await;
10394        mock_ctx.close().await;
10395    }
10396
10397    #[tokio::test(flavor = "multi_thread")]
10398    async fn test_points_vertical() {
10399        let initial_source = "\
10400sketch001 = sketch(on = XY) {
10401  p0 = point(at = [var -2.23mm, var 3.1mm])
10402  pf = point(at = [4, 4])
10403}
10404";
10405
10406        let program = Program::parse(initial_source).unwrap().0.unwrap();
10407
10408        let mut frontend = FrontendState::new();
10409
10410        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10411        let mock_ctx = ExecutorContext::new_mock(None).await;
10412        let version = Version(0);
10413
10414        frontend.hack_set_program(&ctx, program).await.unwrap();
10415        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10416        let sketch_id = sketch_object.id;
10417        let sketch = expect_sketch(sketch_object);
10418        let point_ids = vec![
10419            sketch.segments.first().unwrap().to_owned(),
10420            sketch.segments.get(1).unwrap().to_owned(),
10421        ];
10422
10423        let constraint = Constraint::Vertical(Vertical::Points {
10424            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10425        });
10426        let (src_delta, scene_delta) = frontend
10427            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10428            .await
10429            .unwrap();
10430        assert_eq!(
10431            src_delta.text.as_str(),
10432            "\
10433sketch001 = sketch(on = XY) {
10434  p0 = point(at = [var -2.23mm, var 3.1mm])
10435  pf = point(at = [4, 4])
10436  vertical([p0, pf])
10437}
10438"
10439        );
10440        assert_eq!(
10441            scene_delta.new_graph.objects.len(),
10442            5,
10443            "{:#?}",
10444            scene_delta.new_graph.objects
10445        );
10446
10447        ctx.close().await;
10448        mock_ctx.close().await;
10449    }
10450
10451    #[tokio::test(flavor = "multi_thread")]
10452    async fn test_points_horizontal() {
10453        let initial_source = "\
10454sketch001 = sketch(on = XY) {
10455  p0 = point(at = [var -2.23mm, var 3.1mm])
10456  pf = point(at = [4, 4])
10457}
10458";
10459
10460        let program = Program::parse(initial_source).unwrap().0.unwrap();
10461
10462        let mut frontend = FrontendState::new();
10463
10464        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10465        let mock_ctx = ExecutorContext::new_mock(None).await;
10466        let version = Version(0);
10467
10468        frontend.hack_set_program(&ctx, program).await.unwrap();
10469        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10470        let sketch_id = sketch_object.id;
10471        let sketch = expect_sketch(sketch_object);
10472        let point_ids = vec![
10473            sketch.segments.first().unwrap().to_owned(),
10474            sketch.segments.get(1).unwrap().to_owned(),
10475        ];
10476
10477        let constraint = Constraint::Horizontal(Horizontal::Points {
10478            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
10479        });
10480        let (src_delta, scene_delta) = frontend
10481            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10482            .await
10483            .unwrap();
10484        assert_eq!(
10485            src_delta.text.as_str(),
10486            "\
10487sketch001 = sketch(on = XY) {
10488  p0 = point(at = [var -2.23mm, var 3.1mm])
10489  pf = point(at = [4, 4])
10490  horizontal([p0, pf])
10491}
10492"
10493        );
10494        assert_eq!(
10495            scene_delta.new_graph.objects.len(),
10496            5,
10497            "{:#?}",
10498            scene_delta.new_graph.objects
10499        );
10500
10501        ctx.close().await;
10502        mock_ctx.close().await;
10503    }
10504
10505    #[tokio::test(flavor = "multi_thread")]
10506    async fn test_point_horizontal_with_origin() {
10507        let initial_source = "\
10508sketch001 = sketch(on = XY) {
10509  p0 = point(at = [var -2.23mm, var 3.1mm])
10510}
10511";
10512
10513        let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515        let mut frontend = FrontendState::new();
10516
10517        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10518        let mock_ctx = ExecutorContext::new_mock(None).await;
10519        let version = Version(0);
10520
10521        frontend.hack_set_program(&ctx, program).await.unwrap();
10522        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523        let sketch_id = sketch_object.id;
10524        let sketch = expect_sketch(sketch_object);
10525        let point_id = *sketch.segments.first().unwrap();
10526
10527        let constraint = Constraint::Horizontal(Horizontal::Points {
10528            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
10529        });
10530        let (src_delta, scene_delta) = frontend
10531            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10532            .await
10533            .unwrap();
10534        assert_eq!(
10535            src_delta.text.as_str(),
10536            "\
10537sketch001 = sketch(on = XY) {
10538  p0 = point(at = [var -2.23mm, var 3.1mm])
10539  horizontal([p0, ORIGIN])
10540}
10541"
10542        );
10543        assert_eq!(
10544            scene_delta.new_graph.objects.len(),
10545            4,
10546            "{:#?}",
10547            scene_delta.new_graph.objects
10548        );
10549
10550        ctx.close().await;
10551        mock_ctx.close().await;
10552    }
10553
10554    #[tokio::test(flavor = "multi_thread")]
10555    async fn test_lines_equal_length() {
10556        let initial_source = "\
10557sketch(on = XY) {
10558  line(start = [var 1, var 2], end = [var 3, var 4])
10559  line(start = [var 5, var 6], end = [var 7, var 8])
10560}
10561";
10562
10563        let program = Program::parse(initial_source).unwrap().0.unwrap();
10564
10565        let mut frontend = FrontendState::new();
10566
10567        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10568        let mock_ctx = ExecutorContext::new_mock(None).await;
10569        let version = Version(0);
10570
10571        frontend.hack_set_program(&ctx, program).await.unwrap();
10572        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10573        let sketch_id = sketch_object.id;
10574        let sketch = expect_sketch(sketch_object);
10575        let line1_id = *sketch.segments.get(2).unwrap();
10576        let line2_id = *sketch.segments.get(5).unwrap();
10577
10578        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10579            lines: vec![line1_id, line2_id],
10580        });
10581        let (src_delta, scene_delta) = frontend
10582            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10583            .await
10584            .unwrap();
10585        assert_eq!(
10586            src_delta.text.as_str(),
10587            "\
10588sketch(on = XY) {
10589  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10590  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10591  equalLength([line1, line2])
10592}
10593"
10594        );
10595        assert_eq!(
10596            scene_delta.new_graph.objects.len(),
10597            9,
10598            "{:#?}",
10599            scene_delta.new_graph.objects
10600        );
10601
10602        ctx.close().await;
10603        mock_ctx.close().await;
10604    }
10605
10606    #[tokio::test(flavor = "multi_thread")]
10607    async fn test_add_constraint_multi_line_equal_length() {
10608        let initial_source = "\
10609sketch(on = XY) {
10610  line(start = [var 1, var 2], end = [var 3, var 4])
10611  line(start = [var 5, var 6], end = [var 7, var 8])
10612  line(start = [var 9, var 10], end = [var 11, var 12])
10613}
10614";
10615
10616        let program = Program::parse(initial_source).unwrap().0.unwrap();
10617
10618        let mut frontend = FrontendState::new();
10619        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10620        let mock_ctx = ExecutorContext::new_mock(None).await;
10621        let version = Version(0);
10622
10623        frontend.hack_set_program(&ctx, program).await.unwrap();
10624        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625        let sketch_id = sketch_object.id;
10626        let sketch = expect_sketch(sketch_object);
10627        let line1_id = *sketch.segments.get(2).unwrap();
10628        let line2_id = *sketch.segments.get(5).unwrap();
10629        let line3_id = *sketch.segments.get(8).unwrap();
10630
10631        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
10632            lines: vec![line1_id, line2_id, line3_id],
10633        });
10634        let (src_delta, scene_delta) = frontend
10635            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10636            .await
10637            .unwrap();
10638        assert_eq!(
10639            src_delta.text.as_str(),
10640            "\
10641sketch(on = XY) {
10642  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10643  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10644  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10645  equalLength([line1, line2, line3])
10646}
10647"
10648        );
10649        let constraints = scene_delta
10650            .new_graph
10651            .objects
10652            .iter()
10653            .filter_map(|obj| {
10654                let ObjectKind::Constraint { constraint } = &obj.kind else {
10655                    return None;
10656                };
10657                Some(constraint)
10658            })
10659            .collect::<Vec<_>>();
10660
10661        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
10662        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
10663            panic!("expected equal length constraint, got {:?}", constraints[0]);
10664        };
10665        assert_eq!(lines_equal_length.lines.len(), 3);
10666
10667        ctx.close().await;
10668        mock_ctx.close().await;
10669    }
10670
10671    #[tokio::test(flavor = "multi_thread")]
10672    async fn test_lines_parallel() {
10673        let initial_source = "\
10674sketch(on = XY) {
10675  line(start = [var 1, var 2], end = [var 3, var 4])
10676  line(start = [var 5, var 6], end = [var 7, var 8])
10677}
10678";
10679
10680        let program = Program::parse(initial_source).unwrap().0.unwrap();
10681
10682        let mut frontend = FrontendState::new();
10683
10684        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10685        let mock_ctx = ExecutorContext::new_mock(None).await;
10686        let version = Version(0);
10687
10688        frontend.hack_set_program(&ctx, program).await.unwrap();
10689        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10690        let sketch_id = sketch_object.id;
10691        let sketch = expect_sketch(sketch_object);
10692        let line1_id = *sketch.segments.get(2).unwrap();
10693        let line2_id = *sketch.segments.get(5).unwrap();
10694
10695        let constraint = Constraint::Parallel(Parallel {
10696            lines: vec![line1_id, line2_id],
10697        });
10698        let (src_delta, scene_delta) = frontend
10699            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10700            .await
10701            .unwrap();
10702        assert_eq!(
10703            src_delta.text.as_str(),
10704            "\
10705sketch(on = XY) {
10706  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10707  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10708  parallel([line1, line2])
10709}
10710"
10711        );
10712        assert_eq!(
10713            scene_delta.new_graph.objects.len(),
10714            9,
10715            "{:#?}",
10716            scene_delta.new_graph.objects
10717        );
10718
10719        ctx.close().await;
10720        mock_ctx.close().await;
10721    }
10722
10723    #[tokio::test(flavor = "multi_thread")]
10724    async fn test_lines_parallel_multiline() {
10725        let initial_source = "\
10726sketch(on = XY) {
10727  line(start = [var 1, var 2], end = [var 3, var 4])
10728  line(start = [var 5, var 6], end = [var 7, var 8])
10729  line(start = [var 9, var 10], end = [var 11, var 12])
10730}
10731";
10732
10733        let program = Program::parse(initial_source).unwrap().0.unwrap();
10734
10735        let mut frontend = FrontendState::new();
10736
10737        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10738        let mock_ctx = ExecutorContext::new_mock(None).await;
10739        let version = Version(0);
10740
10741        frontend.hack_set_program(&ctx, program).await.unwrap();
10742        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10743        let sketch_id = sketch_object.id;
10744        let sketch = expect_sketch(sketch_object);
10745        let line1_id = *sketch.segments.get(2).unwrap();
10746        let line2_id = *sketch.segments.get(5).unwrap();
10747        let line3_id = *sketch.segments.get(8).unwrap();
10748
10749        let constraint = Constraint::Parallel(Parallel {
10750            lines: vec![line1_id, line2_id, line3_id],
10751        });
10752        let (src_delta, scene_delta) = frontend
10753            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10754            .await
10755            .unwrap();
10756        assert_eq!(
10757            src_delta.text.as_str(),
10758            "\
10759sketch(on = XY) {
10760  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10761  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10762  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10763  parallel([line1, line2, line3])
10764}
10765"
10766        );
10767
10768        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10769        let sketch = expect_sketch(sketch_object);
10770        assert_eq!(sketch.constraints.len(), 1);
10771
10772        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10773        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10774            panic!("Expected constraint object");
10775        };
10776        let Constraint::Parallel(parallel) = constraint else {
10777            panic!("Expected parallel constraint");
10778        };
10779        assert_eq!(parallel.lines.len(), 3);
10780
10781        ctx.close().await;
10782        mock_ctx.close().await;
10783    }
10784
10785    #[tokio::test(flavor = "multi_thread")]
10786    async fn test_lines_perpendicular() {
10787        let initial_source = "\
10788sketch(on = XY) {
10789  line(start = [var 1, var 2], end = [var 3, var 4])
10790  line(start = [var 5, var 6], end = [var 7, var 8])
10791}
10792";
10793
10794        let program = Program::parse(initial_source).unwrap().0.unwrap();
10795
10796        let mut frontend = FrontendState::new();
10797
10798        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10799        let mock_ctx = ExecutorContext::new_mock(None).await;
10800        let version = Version(0);
10801
10802        frontend.hack_set_program(&ctx, program).await.unwrap();
10803        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10804        let sketch_id = sketch_object.id;
10805        let sketch = expect_sketch(sketch_object);
10806        let line1_id = *sketch.segments.get(2).unwrap();
10807        let line2_id = *sketch.segments.get(5).unwrap();
10808
10809        let constraint = Constraint::Perpendicular(Perpendicular {
10810            lines: vec![line1_id, line2_id],
10811        });
10812        let (src_delta, scene_delta) = frontend
10813            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10814            .await
10815            .unwrap();
10816        assert_eq!(
10817            src_delta.text.as_str(),
10818            "\
10819sketch(on = XY) {
10820  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10821  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10822  perpendicular([line1, line2])
10823}
10824"
10825        );
10826        assert_eq!(
10827            scene_delta.new_graph.objects.len(),
10828            9,
10829            "{:#?}",
10830            scene_delta.new_graph.objects
10831        );
10832
10833        ctx.close().await;
10834        mock_ctx.close().await;
10835    }
10836
10837    #[tokio::test(flavor = "multi_thread")]
10838    async fn test_lines_angle() {
10839        let initial_source = "\
10840sketch(on = XY) {
10841  line(start = [var 1, var 2], end = [var 3, var 4])
10842  line(start = [var 5, var 6], end = [var 7, var 8])
10843}
10844";
10845
10846        let program = Program::parse(initial_source).unwrap().0.unwrap();
10847
10848        let mut frontend = FrontendState::new();
10849
10850        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10851        let mock_ctx = ExecutorContext::new_mock(None).await;
10852        let version = Version(0);
10853
10854        frontend.hack_set_program(&ctx, program).await.unwrap();
10855        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10856        let sketch_id = sketch_object.id;
10857        let sketch = expect_sketch(sketch_object);
10858        let line1_id = *sketch.segments.get(2).unwrap();
10859        let line2_id = *sketch.segments.get(5).unwrap();
10860
10861        let constraint = Constraint::Angle(Angle {
10862            lines: vec![line1_id, line2_id],
10863            angle: Number {
10864                value: 30.0,
10865                units: NumericSuffix::Deg,
10866            },
10867            source: Default::default(),
10868        });
10869        let (src_delta, scene_delta) = frontend
10870            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10871            .await
10872            .unwrap();
10873        assert_eq!(
10874            src_delta.text.as_str(),
10875            // The lack indentation is a formatter bug.
10876            "\
10877sketch(on = XY) {
10878  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10879  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10880  angle([line1, line2]) == 30deg
10881}
10882"
10883        );
10884        assert_eq!(
10885            scene_delta.new_graph.objects.len(),
10886            9,
10887            "{:#?}",
10888            scene_delta.new_graph.objects
10889        );
10890
10891        ctx.close().await;
10892        mock_ctx.close().await;
10893    }
10894
10895    #[tokio::test(flavor = "multi_thread")]
10896    async fn test_segments_tangent() {
10897        let initial_source = "\
10898sketch(on = XY) {
10899  line(start = [var 1, var 2], end = [var 3, var 4])
10900  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10901}
10902";
10903
10904        let program = Program::parse(initial_source).unwrap().0.unwrap();
10905
10906        let mut frontend = FrontendState::new();
10907
10908        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10909        let mock_ctx = ExecutorContext::new_mock(None).await;
10910        let version = Version(0);
10911
10912        frontend.hack_set_program(&ctx, program).await.unwrap();
10913        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10914        let sketch_id = sketch_object.id;
10915        let sketch = expect_sketch(sketch_object);
10916        let line1_id = *sketch.segments.get(2).unwrap();
10917        let arc1_id = *sketch.segments.get(6).unwrap();
10918
10919        let constraint = Constraint::Tangent(Tangent {
10920            input: vec![line1_id, arc1_id],
10921        });
10922        let (src_delta, scene_delta) = frontend
10923            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10924            .await
10925            .unwrap();
10926        assert_eq!(
10927            src_delta.text.as_str(),
10928            "\
10929sketch(on = XY) {
10930  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10931  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
10932  tangent([line1, arc1])
10933}
10934"
10935        );
10936        assert_eq!(
10937            scene_delta.new_graph.objects.len(),
10938            10,
10939            "{:#?}",
10940            scene_delta.new_graph.objects
10941        );
10942
10943        ctx.close().await;
10944        mock_ctx.close().await;
10945    }
10946
10947    #[tokio::test(flavor = "multi_thread")]
10948    async fn test_point_midpoint() {
10949        let initial_source = "\
10950sketch(on = XY) {
10951  point(at = [var 1, var 1])
10952  line(start = [var 0, var 0], end = [var 6, var 4])
10953}
10954";
10955
10956        let program = Program::parse(initial_source).unwrap().0.unwrap();
10957
10958        let mut frontend = FrontendState::new();
10959
10960        let ctx = ExecutorContext::new_mock(None).await;
10961        let version = Version(0);
10962
10963        frontend.program = program.clone();
10964        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10965        frontend.update_state_after_exec(outcome, true);
10966        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10967        let sketch_id = sketch_object.id;
10968        let sketch = expect_sketch(sketch_object);
10969        let point_id = *sketch.segments.first().unwrap();
10970        let line_id = *sketch.segments.get(3).unwrap();
10971
10972        let constraint = Constraint::Midpoint(Midpoint {
10973            point: point_id,
10974            segment: line_id,
10975        });
10976        let (src_delta, scene_delta) = frontend
10977            .add_constraint(&ctx, version, sketch_id, constraint)
10978            .await
10979            .unwrap();
10980        assert_eq!(
10981            src_delta.text.as_str(),
10982            "\
10983sketch(on = XY) {
10984  point1 = point(at = [var 1, var 1])
10985  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10986  midpoint(line1, point = point1)
10987}
10988"
10989        );
10990        assert_eq!(
10991            scene_delta.new_graph.objects.len(),
10992            7,
10993            "{:#?}",
10994            scene_delta.new_graph.objects
10995        );
10996
10997        ctx.close().await;
10998    }
10999
11000    #[tokio::test(flavor = "multi_thread")]
11001    async fn test_segments_symmetric() {
11002        let initial_source = "\
11003sketch(on = XY) {
11004  line(start = [var 0, var 0], end = [var 0, var 4])
11005  line(start = [var 4, var 0], end = [var 4, var 4])
11006  line(start = [var 2, var -1], end = [var 2, var 5])
11007}
11008";
11009
11010        let program = Program::parse(initial_source).unwrap().0.unwrap();
11011
11012        let mut frontend = FrontendState::new();
11013
11014        let ctx = ExecutorContext::new_mock(None).await;
11015        let version = Version(0);
11016
11017        frontend.program = program.clone();
11018        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11019        frontend.update_state_after_exec(outcome, true);
11020        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11021        let sketch_id = sketch_object.id;
11022        let sketch = expect_sketch(sketch_object);
11023        let line1_id = *sketch.segments.get(2).unwrap();
11024        let line2_id = *sketch.segments.get(5).unwrap();
11025        let axis_id = *sketch.segments.get(8).unwrap();
11026
11027        let constraint = Constraint::Symmetric(Symmetric {
11028            input: vec![line1_id, line2_id],
11029            axis: axis_id,
11030        });
11031        let (src_delta, scene_delta) = frontend
11032            .add_constraint(&ctx, version, sketch_id, constraint)
11033            .await
11034            .unwrap();
11035        assert_eq!(
11036            src_delta.text.as_str(),
11037            "\
11038sketch(on = XY) {
11039  line1 = line(start = [var 0, var 0], end = [var 0, var 4])
11040  line2 = line(start = [var 4, var 0], end = [var 4, var 4])
11041  line3 = line(start = [var 2, var -1], end = [var 2, var 5])
11042  symmetric([line1, line2], axis = line3)
11043}
11044"
11045        );
11046        assert_eq!(
11047            scene_delta.new_graph.objects.len(),
11048            12,
11049            "{:#?}",
11050            scene_delta.new_graph.objects
11051        );
11052
11053        ctx.close().await;
11054    }
11055
11056    #[tokio::test(flavor = "multi_thread")]
11057    async fn test_point_arc_midpoint() {
11058        let initial_source = "\
11059sketch(on = XY) {
11060  point(at = [var 6, var 3])
11061  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11062}
11063";
11064
11065        let program = Program::parse(initial_source).unwrap().0.unwrap();
11066
11067        let mut frontend = FrontendState::new();
11068
11069        let ctx = ExecutorContext::new_mock(None).await;
11070        let version = Version(0);
11071
11072        frontend.program = program.clone();
11073        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11074        frontend.update_state_after_exec(outcome, true);
11075        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11076        let sketch_id = sketch_object.id;
11077        let sketch = expect_sketch(sketch_object);
11078        let point_id = *sketch.segments.first().unwrap();
11079        let arc_id = *sketch.segments.get(4).unwrap();
11080
11081        let constraint = Constraint::Midpoint(Midpoint {
11082            point: point_id,
11083            segment: arc_id,
11084        });
11085        let (src_delta, scene_delta) = frontend
11086            .add_constraint(&ctx, version, sketch_id, constraint)
11087            .await
11088            .unwrap();
11089        assert_eq!(
11090            src_delta.text.as_str(),
11091            "\
11092sketch(on = XY) {
11093  point1 = point(at = [var 6, var 3])
11094  arc1 = arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
11095  midpoint(arc1, point = point1)
11096}
11097"
11098        );
11099        assert_eq!(
11100            scene_delta.new_graph.objects.len(),
11101            8,
11102            "{:#?}",
11103            scene_delta.new_graph.objects
11104        );
11105
11106        ctx.close().await;
11107    }
11108
11109    #[tokio::test(flavor = "multi_thread")]
11110    async fn test_segments_symmetric_arcs() {
11111        let initial_source = "\
11112sketch(on = XY) {
11113  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11114  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11115  line(start = [var 0, var -10], end = [var 0, var 10])
11116}
11117";
11118
11119        let program = Program::parse(initial_source).unwrap().0.unwrap();
11120
11121        let mut frontend = FrontendState::new();
11122
11123        let ctx = ExecutorContext::new_mock(None).await;
11124        let version = Version(0);
11125
11126        frontend.program = program.clone();
11127        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11128        frontend.update_state_after_exec(outcome, true);
11129        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11130        let sketch_id = sketch_object.id;
11131        let sketch = expect_sketch(sketch_object);
11132        let arc1_id = *sketch.segments.get(3).unwrap();
11133        let arc2_id = *sketch.segments.get(7).unwrap();
11134        let axis_id = *sketch.segments.get(10).unwrap();
11135
11136        let constraint = Constraint::Symmetric(Symmetric {
11137            input: vec![arc1_id, arc2_id],
11138            axis: axis_id,
11139        });
11140        let (src_delta, scene_delta) = frontend
11141            .add_constraint(&ctx, version, sketch_id, constraint)
11142            .await
11143            .unwrap();
11144        assert_eq!(
11145            src_delta.text.as_str(),
11146            "\
11147sketch(on = XY) {
11148  arc1 = arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
11149  arc2 = arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
11150  line1 = line(start = [var 0, var -10], end = [var 0, var 10])
11151  symmetric([arc1, arc2], axis = line1)
11152}
11153"
11154        );
11155        assert_eq!(
11156            scene_delta.new_graph.objects.len(),
11157            14,
11158            "{:#?}",
11159            scene_delta.new_graph.objects
11160        );
11161
11162        ctx.close().await;
11163    }
11164
11165    #[tokio::test(flavor = "multi_thread")]
11166    async fn test_sketch_on_face_simple() {
11167        let initial_source = "\
11168len = 2mm
11169cube = startSketchOn(XY)
11170  |> startProfile(at = [0, 0])
11171  |> line(end = [len, 0], tag = $side)
11172  |> line(end = [0, len])
11173  |> line(end = [-len, 0])
11174  |> line(end = [0, -len])
11175  |> close()
11176  |> extrude(length = len)
11177
11178face = faceOf(cube, face = side)
11179";
11180
11181        let program = Program::parse(initial_source).unwrap().0.unwrap();
11182
11183        let mut frontend = FrontendState::new();
11184
11185        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11186        let mock_ctx = ExecutorContext::new_mock(None).await;
11187        let version = Version(0);
11188
11189        frontend.hack_set_program(&ctx, program).await.unwrap();
11190        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
11191        let face_id = face_object.id;
11192
11193        let sketch_args = SketchCtor {
11194            on: Plane::Object(face_id),
11195        };
11196        let (_src_delta, scene_delta, sketch_id) = frontend
11197            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11198            .await
11199            .unwrap();
11200        assert_eq!(sketch_id, ObjectId(2));
11201        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11202        let sketch_object = &scene_delta.new_graph.objects[2];
11203        assert_eq!(sketch_object.id, ObjectId(2));
11204        assert_eq!(
11205            sketch_object.kind,
11206            ObjectKind::Sketch(Sketch {
11207                args: SketchCtor {
11208                    on: Plane::Object(face_id),
11209                },
11210                plane: face_id,
11211                segments: vec![],
11212                constraints: vec![],
11213            })
11214        );
11215        assert_eq!(scene_delta.new_graph.objects.len(), 8);
11216
11217        ctx.close().await;
11218        mock_ctx.close().await;
11219    }
11220
11221    #[tokio::test(flavor = "multi_thread")]
11222    async fn test_sketch_on_wall_artifact_from_region_extrude() {
11223        let initial_source = "\
11224s = sketch(on = YZ) {
11225  line1 = line(start = [0, 0], end = [0, 1])
11226  line2 = line(start = [0, 1], end = [1, 1])
11227  line3 = line(start = [1, 1], end = [0, 0])
11228}
11229region001 = region(point = [0.1, 0.1], sketch = s)
11230extrude001 = extrude(region001, length = 5)
11231";
11232
11233        let program = Program::parse(initial_source).unwrap().0.unwrap();
11234
11235        let mut frontend = FrontendState::new();
11236        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11237        let version = Version(0);
11238
11239        frontend.hack_set_program(&ctx, program).await.unwrap();
11240        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11241
11242        let sketch_args = SketchCtor {
11243            on: Plane::Object(wall_object_id),
11244        };
11245        let (src_delta, _scene_delta, _sketch_id) = frontend
11246            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11247            .await
11248            .unwrap();
11249        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11250
11251        ctx.close().await;
11252    }
11253
11254    #[tokio::test(flavor = "multi_thread")]
11255    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
11256        let initial_source = "\
11257sketch001 = sketch(on = YZ) {
11258  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
11259  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
11260  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
11261  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
11262  coincident([line1.end, line2.start])
11263  coincident([line2.end, line3.start])
11264  coincident([line3.end, line4.start])
11265  coincident([line4.end, line1.start])
11266  parallel([line2, line4])
11267  parallel([line3, line1])
11268  perpendicular([line1, line2])
11269  horizontal(line3)
11270  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
11271}
11272region001 = region(point = [3.1, 3.74], sketch = sketch001)
11273extrude001 = extrude(region001, length = 5)
11274";
11275
11276        let program = Program::parse(initial_source).unwrap().0.unwrap();
11277
11278        let mut frontend = FrontendState::new();
11279        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11280        let version = Version(0);
11281
11282        frontend.hack_set_program(&ctx, program).await.unwrap();
11283        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
11284
11285        let sketch_args = SketchCtor {
11286            on: Plane::Object(wall_object_id),
11287        };
11288        let (src_delta, _scene_delta, _sketch_id) = frontend
11289            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11290            .await
11291            .unwrap();
11292        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
11293
11294        ctx.close().await;
11295    }
11296
11297    #[tokio::test(flavor = "multi_thread")]
11298    async fn test_sketch_on_plane_incremental() {
11299        let initial_source = "\
11300len = 2mm
11301cube = startSketchOn(XY)
11302  |> startProfile(at = [0, 0])
11303  |> line(end = [len, 0], tag = $side)
11304  |> line(end = [0, len])
11305  |> line(end = [-len, 0])
11306  |> line(end = [0, -len])
11307  |> close()
11308  |> extrude(length = len)
11309
11310plane = planeOf(cube, face = side)
11311";
11312
11313        let program = Program::parse(initial_source).unwrap().0.unwrap();
11314
11315        let mut frontend = FrontendState::new();
11316
11317        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11318        let mock_ctx = ExecutorContext::new_mock(None).await;
11319        let version = Version(0);
11320
11321        frontend.hack_set_program(&ctx, program).await.unwrap();
11322        // Find the last plane since the first plane is the XY plane.
11323        let plane_object = frontend
11324            .scene_graph
11325            .objects
11326            .iter()
11327            .rev()
11328            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
11329            .unwrap();
11330        let plane_id = plane_object.id;
11331
11332        let sketch_args = SketchCtor {
11333            on: Plane::Object(plane_id),
11334        };
11335        let (src_delta, scene_delta, sketch_id) = frontend
11336            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11337            .await
11338            .unwrap();
11339        assert_eq!(
11340            src_delta.text.as_str(),
11341            "\
11342len = 2mm
11343cube = startSketchOn(XY)
11344  |> startProfile(at = [0, 0])
11345  |> line(end = [len, 0], tag = $side)
11346  |> line(end = [0, len])
11347  |> line(end = [-len, 0])
11348  |> line(end = [0, -len])
11349  |> close()
11350  |> extrude(length = len)
11351
11352plane = planeOf(cube, face = side)
11353sketch001 = sketch(on = plane) {
11354}
11355"
11356        );
11357        assert_eq!(sketch_id, ObjectId(2));
11358        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
11359        let sketch_object = &scene_delta.new_graph.objects[2];
11360        assert_eq!(sketch_object.id, ObjectId(2));
11361        assert_eq!(
11362            sketch_object.kind,
11363            ObjectKind::Sketch(Sketch {
11364                args: SketchCtor {
11365                    on: Plane::Object(plane_id),
11366                },
11367                plane: plane_id,
11368                segments: vec![],
11369                constraints: vec![],
11370            })
11371        );
11372        assert_eq!(scene_delta.new_graph.objects.len(), 9);
11373
11374        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
11375        assert_eq!(plane_object.id, plane_id);
11376        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
11377
11378        ctx.close().await;
11379        mock_ctx.close().await;
11380    }
11381
11382    #[tokio::test(flavor = "multi_thread")]
11383    async fn test_new_sketch_uses_unique_variable_name() {
11384        let initial_source = "\
11385sketch1 = sketch(on = XY) {
11386}
11387";
11388
11389        let program = Program::parse(initial_source).unwrap().0.unwrap();
11390
11391        let mut frontend = FrontendState::new();
11392        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11393        let version = Version(0);
11394
11395        frontend.hack_set_program(&ctx, program).await.unwrap();
11396
11397        let sketch_args = SketchCtor {
11398            on: Plane::Default(PlaneName::Yz),
11399        };
11400        let (src_delta, _, _) = frontend
11401            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11402            .await
11403            .unwrap();
11404
11405        assert_eq!(
11406            src_delta.text.as_str(),
11407            "\
11408sketch1 = sketch(on = XY) {
11409}
11410sketch001 = sketch(on = YZ) {
11411}
11412"
11413        );
11414
11415        ctx.close().await;
11416    }
11417
11418    #[tokio::test(flavor = "multi_thread")]
11419    async fn test_new_sketch_twice_using_same_plane() {
11420        let initial_source = "\
11421sketch1 = sketch(on = XY) {
11422}
11423";
11424
11425        let program = Program::parse(initial_source).unwrap().0.unwrap();
11426
11427        let mut frontend = FrontendState::new();
11428        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11429        let version = Version(0);
11430
11431        frontend.hack_set_program(&ctx, program).await.unwrap();
11432
11433        let sketch_args = SketchCtor {
11434            on: Plane::Default(PlaneName::Xy),
11435        };
11436        let (src_delta, _, _) = frontend
11437            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
11438            .await
11439            .unwrap();
11440
11441        assert_eq!(
11442            src_delta.text.as_str(),
11443            "\
11444sketch1 = sketch(on = XY) {
11445}
11446sketch001 = sketch(on = XY) {
11447}
11448"
11449        );
11450
11451        ctx.close().await;
11452    }
11453
11454    #[tokio::test(flavor = "multi_thread")]
11455    async fn test_sketch_mode_reuses_cached_on_expression() {
11456        let initial_source = "\
11457width = 2mm
11458sketch(on = offsetPlane(XY, offset = width)) {
11459  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
11460  distance([line1.start, line1.end]) == width
11461}
11462";
11463        let program = Program::parse(initial_source).unwrap().0.unwrap();
11464
11465        let mut frontend = FrontendState::new();
11466        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11467        let mock_ctx = ExecutorContext::new_mock(None).await;
11468        let version = Version(0);
11469        let project_id = ProjectId(0);
11470        let file_id = FileId(0);
11471
11472        frontend.hack_set_program(&ctx, program).await.unwrap();
11473        let initial_object_count = frontend.scene_graph.objects.len();
11474        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
11475            .expect("Expected sketch object to exist")
11476            .id;
11477
11478        // Entering sketch mode should reuse cached `on` expression state
11479        // (offsetPlane result), not fail or create extra on-surface objects.
11480        let scene_delta = frontend
11481            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11482            .await
11483            .unwrap();
11484        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11485
11486        // A follow-up sketch-mode execution should keep the same stable object
11487        // graph shape as well.
11488        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
11489        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
11490
11491        ctx.close().await;
11492        mock_ctx.close().await;
11493    }
11494
11495    #[tokio::test(flavor = "multi_thread")]
11496    async fn test_multiple_sketch_blocks() {
11497        let initial_source = "\
11498// Cube that requires the engine.
11499width = 2
11500sketch001 = startSketchOn(XY)
11501profile001 = startProfile(sketch001, at = [0, 0])
11502  |> yLine(length = width, tag = $seg1)
11503  |> xLine(length = width)
11504  |> yLine(length = -width)
11505  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11506  |> close()
11507extrude001 = extrude(profile001, length = width)
11508
11509// Get a value that requires the engine.
11510x = segLen(seg1)
11511
11512// Triangle with side length 2*x.
11513sketch(on = XY) {
11514  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11515  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11516  coincident([line1.end, line2.start])
11517  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11518  coincident([line2.end, line3.start])
11519  coincident([line3.end, line1.start])
11520  equalLength([line3, line1])
11521  equalLength([line1, line2])
11522  distance([line1.start, line1.end]) == 2*x
11523}
11524
11525// Line segment with length x.
11526sketch2 = sketch(on = XY) {
11527  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11528  distance([line1.start, line1.end]) == x
11529}
11530";
11531
11532        let program = Program::parse(initial_source).unwrap().0.unwrap();
11533
11534        let mut frontend = FrontendState::new();
11535
11536        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11537        let mock_ctx = ExecutorContext::new_mock(None).await;
11538        let version = Version(0);
11539        let project_id = ProjectId(0);
11540        let file_id = FileId(0);
11541
11542        frontend.hack_set_program(&ctx, program).await.unwrap();
11543        let sketch_objects = frontend
11544            .scene_graph
11545            .objects
11546            .iter()
11547            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
11548            .collect::<Vec<_>>();
11549        let sketch1_id = sketch_objects.first().unwrap().id;
11550        let sketch2_id = sketch_objects.get(1).unwrap().id;
11551        // First point in sketch1.
11552        let point1_id = ObjectId(sketch1_id.0 + 1);
11553        // First point in sketch2.
11554        let point2_id = ObjectId(sketch2_id.0 + 1);
11555
11556        // Edit the first sketch. Objects before the sketch block should be
11557        // present from execution cache so that we can sketch on prior planes,
11558        // for example. Objects after the first sketch block should not be
11559        // present since those statements are skipped in sketch mode.
11560        //
11561        // - startSketchOn(XY) Plane 1
11562        // - sketch on=XY Plane 1
11563        // - Sketch block 16
11564        let scene_delta = frontend
11565            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11566            .await
11567            .unwrap();
11568        assert_eq!(
11569            scene_delta.new_graph.objects.len(),
11570            18,
11571            "{:#?}",
11572            scene_delta.new_graph.objects
11573        );
11574
11575        // Edit a point in the first sketch.
11576        let point_ctor = PointCtor {
11577            position: Point2d {
11578                x: Expr::Var(Number {
11579                    value: 1.0,
11580                    units: NumericSuffix::Mm,
11581                }),
11582                y: Expr::Var(Number {
11583                    value: 2.0,
11584                    units: NumericSuffix::Mm,
11585                }),
11586            },
11587        };
11588        let segments = vec![ExistingSegmentCtor {
11589            id: point1_id,
11590            ctor: SegmentCtor::Point(point_ctor),
11591        }];
11592        let (src_delta, _) = frontend
11593            .edit_segments(&mock_ctx, version, sketch1_id, segments)
11594            .await
11595            .unwrap();
11596        // Only the first sketch block changes.
11597        assert_eq!(
11598            src_delta.text.as_str(),
11599            "\
11600// Cube that requires the engine.
11601width = 2
11602sketch001 = startSketchOn(XY)
11603profile001 = startProfile(sketch001, at = [0, 0])
11604  |> yLine(length = width, tag = $seg1)
11605  |> xLine(length = width)
11606  |> yLine(length = -width)
11607  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11608  |> close()
11609extrude001 = extrude(profile001, length = width)
11610
11611// Get a value that requires the engine.
11612x = segLen(seg1)
11613
11614// Triangle with side length 2*x.
11615sketch(on = XY) {
11616  line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
11617  line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
11618  coincident([line1.end, line2.start])
11619  line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
11620  coincident([line2.end, line3.start])
11621  coincident([line3.end, line1.start])
11622  equalLength([line3, line1])
11623  equalLength([line1, line2])
11624  distance([line1.start, line1.end]) == 2 * x
11625}
11626
11627// Line segment with length x.
11628sketch2 = sketch(on = XY) {
11629  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11630  distance([line1.start, line1.end]) == x
11631}
11632"
11633        );
11634
11635        // Execute mock to simulate drag end.
11636        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
11637        // Only the first sketch block changes.
11638        assert_eq!(
11639            src_delta.text.as_str(),
11640            "\
11641// Cube that requires the engine.
11642width = 2
11643sketch001 = startSketchOn(XY)
11644profile001 = startProfile(sketch001, at = [0, 0])
11645  |> yLine(length = width, tag = $seg1)
11646  |> xLine(length = width)
11647  |> yLine(length = -width)
11648  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11649  |> close()
11650extrude001 = extrude(profile001, length = width)
11651
11652// Get a value that requires the engine.
11653x = segLen(seg1)
11654
11655// Triangle with side length 2*x.
11656sketch(on = XY) {
11657  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11658  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11659  coincident([line1.end, line2.start])
11660  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11661  coincident([line2.end, line3.start])
11662  coincident([line3.end, line1.start])
11663  equalLength([line3, line1])
11664  equalLength([line1, line2])
11665  distance([line1.start, line1.end]) == 2 * x
11666}
11667
11668// Line segment with length x.
11669sketch2 = sketch(on = XY) {
11670  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
11671  distance([line1.start, line1.end]) == x
11672}
11673"
11674        );
11675        // Exit sketch. Objects from the entire program should be present.
11676        //
11677        // - startSketchOn(XY) Plane 1
11678        // - sketch on=XY Plane 1
11679        // - Sketch block 16
11680        // - sketch on=XY cached
11681        // - Sketch block 5
11682        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11683        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
11684
11685        // Edit the second sketch.
11686        //
11687        // - startSketchOn(XY) Plane 1
11688        // - sketch on=XY Plane 1
11689        // - Sketch block 16
11690        // - sketch on=XY cached
11691        // - Sketch block 5
11692        let scene_delta = frontend
11693            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11694            .await
11695            .unwrap();
11696        assert_eq!(
11697            scene_delta.new_graph.objects.len(),
11698            24,
11699            "{:#?}",
11700            scene_delta.new_graph.objects
11701        );
11702
11703        // Edit a point in the second sketch.
11704        let point_ctor = PointCtor {
11705            position: Point2d {
11706                x: Expr::Var(Number {
11707                    value: 3.0,
11708                    units: NumericSuffix::Mm,
11709                }),
11710                y: Expr::Var(Number {
11711                    value: 4.0,
11712                    units: NumericSuffix::Mm,
11713                }),
11714            },
11715        };
11716        let segments = vec![ExistingSegmentCtor {
11717            id: point2_id,
11718            ctor: SegmentCtor::Point(point_ctor),
11719        }];
11720        let (src_delta, _) = frontend
11721            .edit_segments(&mock_ctx, version, sketch2_id, segments)
11722            .await
11723            .unwrap();
11724        // Only the second sketch block changes.
11725        assert_eq!(
11726            src_delta.text.as_str(),
11727            "\
11728// Cube that requires the engine.
11729width = 2
11730sketch001 = startSketchOn(XY)
11731profile001 = startProfile(sketch001, at = [0, 0])
11732  |> yLine(length = width, tag = $seg1)
11733  |> xLine(length = width)
11734  |> yLine(length = -width)
11735  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11736  |> close()
11737extrude001 = extrude(profile001, length = width)
11738
11739// Get a value that requires the engine.
11740x = segLen(seg1)
11741
11742// Triangle with side length 2*x.
11743sketch(on = XY) {
11744  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11745  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11746  coincident([line1.end, line2.start])
11747  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11748  coincident([line2.end, line3.start])
11749  coincident([line3.end, line1.start])
11750  equalLength([line3, line1])
11751  equalLength([line1, line2])
11752  distance([line1.start, line1.end]) == 2 * x
11753}
11754
11755// Line segment with length x.
11756sketch2 = sketch(on = XY) {
11757  line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
11758  distance([line1.start, line1.end]) == x
11759}
11760"
11761        );
11762
11763        // Execute mock to simulate drag end.
11764        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
11765        // Only the second sketch block changes.
11766        assert_eq!(
11767            src_delta.text.as_str(),
11768            "\
11769// Cube that requires the engine.
11770width = 2
11771sketch001 = startSketchOn(XY)
11772profile001 = startProfile(sketch001, at = [0, 0])
11773  |> yLine(length = width, tag = $seg1)
11774  |> xLine(length = width)
11775  |> yLine(length = -width)
11776  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
11777  |> close()
11778extrude001 = extrude(profile001, length = width)
11779
11780// Get a value that requires the engine.
11781x = segLen(seg1)
11782
11783// Triangle with side length 2*x.
11784sketch(on = XY) {
11785  line1 = line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
11786  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
11787  coincident([line1.end, line2.start])
11788  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
11789  coincident([line2.end, line3.start])
11790  coincident([line3.end, line1.start])
11791  equalLength([line3, line1])
11792  equalLength([line1, line2])
11793  distance([line1.start, line1.end]) == 2 * x
11794}
11795
11796// Line segment with length x.
11797sketch2 = sketch(on = XY) {
11798  line1 = line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
11799  distance([line1.start, line1.end]) == x
11800}
11801"
11802        );
11803
11804        ctx.close().await;
11805        mock_ctx.close().await;
11806    }
11807
11808    #[tokio::test(flavor = "multi_thread")]
11809    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
11810        clear_mem_cache().await;
11811
11812        let source = r#"sketch001 = sketch(on = XZ) {
11813  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
11814}
11815sketch002 = sketch(on = XY) {
11816  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
11817  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
11818  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
11819  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
11820  coincident([line1.end, line2.start])
11821  coincident([line2.end, line3.start])
11822  coincident([line3.end, line4.start])
11823  coincident([line4.end, line1.start])
11824  parallel([line2, line4])
11825  parallel([line3, line1])
11826  perpendicular([line1, line2])
11827  horizontal(line3)
11828  coincident([line1.start, ORIGIN])
11829}
11830"#;
11831
11832        let program = Program::parse(source).unwrap().0.unwrap();
11833        let mut frontend = FrontendState::new();
11834        let ctx = ExecutorContext::new_with_engine(
11835            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
11836            Default::default(),
11837        );
11838        let mock_ctx = ExecutorContext::new_mock(None).await;
11839        let version = Version(0);
11840        let project_id = ProjectId(0);
11841        let file_id = FileId(0);
11842
11843        frontend.hack_set_program(&ctx, program).await.unwrap();
11844        let sketch_objects = frontend
11845            .scene_graph
11846            .objects
11847            .iter()
11848            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
11849            .collect::<Vec<_>>();
11850        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
11851
11852        let sketch1_id = sketch_objects[0].id;
11853        let sketch2_id = sketch_objects[1].id;
11854
11855        frontend
11856            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
11857            .await
11858            .unwrap();
11859        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
11860
11861        let scene_delta = frontend
11862            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
11863            .await
11864            .unwrap();
11865        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
11866
11867        clear_mem_cache().await;
11868        ctx.close().await;
11869        mock_ctx.close().await;
11870    }
11871
11872    // Regression tests: operations on source code with extra whitespace/newlines.
11873    // These test that NodePath-based lookups work correctly when source ranges
11874    // are shifted by extra whitespace that wouldn't be present after formatting.
11875
11876    #[tokio::test(flavor = "multi_thread")]
11877    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
11878        // Extra newlines after @settings line - this shifts all source ranges.
11879        let initial_source = "@settings(defaultLengthUnit = mm)
11880
11881
11882
11883sketch001 = sketch(on = XY) {
11884  point(at = [1in, 2in])
11885}
11886";
11887
11888        let program = Program::parse(initial_source).unwrap().0.unwrap();
11889        let mut frontend = FrontendState::new();
11890
11891        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11892        let mock_ctx = ExecutorContext::new_mock(None).await;
11893        let version = Version(0);
11894        let project_id = ProjectId(0);
11895        let file_id = FileId(0);
11896
11897        frontend.hack_set_program(&ctx, program).await.unwrap();
11898        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11899        let sketch_id = sketch_object.id;
11900
11901        // Edit sketch should succeed despite extra newlines.
11902        frontend
11903            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
11904            .await
11905            .unwrap();
11906
11907        // Add a new point to the sketch.
11908        let point_ctor = PointCtor {
11909            position: Point2d {
11910                x: Expr::Number(Number {
11911                    value: 5.0,
11912                    units: NumericSuffix::Mm,
11913                }),
11914                y: Expr::Number(Number {
11915                    value: 6.0,
11916                    units: NumericSuffix::Mm,
11917                }),
11918            },
11919        };
11920        let segment = SegmentCtor::Point(point_ctor);
11921        let (src_delta, scene_delta) = frontend
11922            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11923            .await
11924            .unwrap();
11925        // After adding a point, the source should be reformatted with standard whitespace.
11926        assert!(
11927            src_delta.text.contains("point(at = [5mm, 6mm])"),
11928            "Expected new point in source, got: {}",
11929            src_delta.text
11930        );
11931        assert!(!scene_delta.new_objects.is_empty());
11932
11933        ctx.close().await;
11934        mock_ctx.close().await;
11935    }
11936
11937    #[tokio::test(flavor = "multi_thread")]
11938    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
11939        // Extra newlines after @settings, with an empty sketch block.
11940        let initial_source = "@settings(defaultLengthUnit = mm)
11941
11942
11943
11944s = sketch(on = XY) {}
11945";
11946
11947        let program = Program::parse(initial_source).unwrap().0.unwrap();
11948        let mut frontend = FrontendState::new();
11949
11950        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11951        let mock_ctx = ExecutorContext::new_mock(None).await;
11952        let version = Version(0);
11953
11954        frontend.hack_set_program(&ctx, program).await.unwrap();
11955        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11956        let sketch_id = sketch_object.id;
11957
11958        let line_ctor = LineCtor {
11959            start: Point2d {
11960                x: Expr::Number(Number {
11961                    value: 0.0,
11962                    units: NumericSuffix::Mm,
11963                }),
11964                y: Expr::Number(Number {
11965                    value: 0.0,
11966                    units: NumericSuffix::Mm,
11967                }),
11968            },
11969            end: Point2d {
11970                x: Expr::Number(Number {
11971                    value: 10.0,
11972                    units: NumericSuffix::Mm,
11973                }),
11974                y: Expr::Number(Number {
11975                    value: 10.0,
11976                    units: NumericSuffix::Mm,
11977                }),
11978            },
11979            construction: None,
11980        };
11981        let segment = SegmentCtor::Line(line_ctor);
11982        let (src_delta, scene_delta) = frontend
11983            .add_segment(&mock_ctx, version, sketch_id, segment, None)
11984            .await
11985            .unwrap();
11986        assert!(
11987            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
11988            "Expected line in source, got: {}",
11989            src_delta.text
11990        );
11991        // Line creates start point, end point, and line segment.
11992        assert_eq!(scene_delta.new_objects.len(), 3);
11993
11994        ctx.close().await;
11995        mock_ctx.close().await;
11996    }
11997
11998    #[tokio::test(flavor = "multi_thread")]
11999    async fn test_extra_newlines_between_operations_edit_line() {
12000        // Extra newlines between @settings and sketch, and inside the sketch block.
12001        let initial_source = "@settings(defaultLengthUnit = mm)
12002
12003
12004sketch001 = sketch(on = XY) {
12005
12006  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12007
12008}
12009";
12010
12011        let program = Program::parse(initial_source).unwrap().0.unwrap();
12012        let mut frontend = FrontendState::new();
12013
12014        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12015        let mock_ctx = ExecutorContext::new_mock(None).await;
12016        let version = Version(0);
12017        let project_id = ProjectId(0);
12018        let file_id = FileId(0);
12019
12020        frontend.hack_set_program(&ctx, program).await.unwrap();
12021        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12022        let sketch_id = sketch_object.id;
12023        let sketch = expect_sketch(sketch_object);
12024
12025        // Extract segment IDs before edit_sketch borrows frontend mutably.
12026        let line_id = sketch
12027            .segments
12028            .iter()
12029            .copied()
12030            .find(|seg_id| {
12031                matches!(
12032                    &frontend.scene_graph.objects[seg_id.0].kind,
12033                    ObjectKind::Segment {
12034                        segment: Segment::Line(_)
12035                    }
12036                )
12037            })
12038            .expect("Expected a line segment in sketch");
12039
12040        // Enter sketch edit mode.
12041        frontend
12042            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12043            .await
12044            .unwrap();
12045
12046        // Edit the line.
12047        let line_ctor = LineCtor {
12048            start: Point2d {
12049                x: Expr::Var(Number {
12050                    value: 1.0,
12051                    units: NumericSuffix::Mm,
12052                }),
12053                y: Expr::Var(Number {
12054                    value: 2.0,
12055                    units: NumericSuffix::Mm,
12056                }),
12057            },
12058            end: Point2d {
12059                x: Expr::Var(Number {
12060                    value: 13.0,
12061                    units: NumericSuffix::Mm,
12062                }),
12063                y: Expr::Var(Number {
12064                    value: 14.0,
12065                    units: NumericSuffix::Mm,
12066                }),
12067            },
12068            construction: None,
12069        };
12070        let segments = vec![ExistingSegmentCtor {
12071            id: line_id,
12072            ctor: SegmentCtor::Line(line_ctor),
12073        }];
12074        let (src_delta, _scene_delta) = frontend
12075            .edit_segments(&mock_ctx, version, sketch_id, segments)
12076            .await
12077            .unwrap();
12078        assert!(
12079            src_delta
12080                .text
12081                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
12082            "Expected edited line in source, got: {}",
12083            src_delta.text
12084        );
12085
12086        ctx.close().await;
12087        mock_ctx.close().await;
12088    }
12089
12090    #[tokio::test(flavor = "multi_thread")]
12091    async fn test_extra_newlines_delete_segment() {
12092        // Extra whitespace before and after the sketch block.
12093        let initial_source = "@settings(defaultLengthUnit = mm)
12094
12095
12096
12097sketch001 = sketch(on = XY) {
12098  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
12099}
12100";
12101
12102        let program = Program::parse(initial_source).unwrap().0.unwrap();
12103        let mut frontend = FrontendState::new();
12104
12105        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12106        let mock_ctx = ExecutorContext::new_mock(None).await;
12107        let version = Version(0);
12108
12109        frontend.hack_set_program(&ctx, program).await.unwrap();
12110        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12111        let sketch_id = sketch_object.id;
12112        let sketch = expect_sketch(sketch_object);
12113
12114        // The sketch should have 3 segments: start point, center point, and the circle.
12115        assert_eq!(sketch.segments.len(), 3);
12116        let circle_id = sketch.segments[2];
12117
12118        // Delete the circle despite extra newlines in original source.
12119        let (src_delta, scene_delta) = frontend
12120            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
12121            .await
12122            .unwrap();
12123        assert!(
12124            src_delta.text.contains("sketch(on = XY) {"),
12125            "Expected sketch block in source, got: {}",
12126            src_delta.text
12127        );
12128        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12129        let new_sketch = expect_sketch(new_sketch_object);
12130        assert_eq!(new_sketch.segments.len(), 0);
12131
12132        ctx.close().await;
12133        mock_ctx.close().await;
12134    }
12135
12136    #[tokio::test(flavor = "multi_thread")]
12137    async fn test_unformatted_source_add_arc() {
12138        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
12139        let initial_source = "@settings(defaultLengthUnit = mm)
12140
12141
12142
12143
12144sketch001 = sketch(on = XY) {
12145}
12146";
12147
12148        let program = Program::parse(initial_source).unwrap().0.unwrap();
12149        let mut frontend = FrontendState::new();
12150
12151        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12152        let mock_ctx = ExecutorContext::new_mock(None).await;
12153        let version = Version(0);
12154
12155        frontend.hack_set_program(&ctx, program).await.unwrap();
12156        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12157        let sketch_id = sketch_object.id;
12158
12159        let arc_ctor = ArcCtor {
12160            start: Point2d {
12161                x: Expr::Var(Number {
12162                    value: 5.0,
12163                    units: NumericSuffix::Mm,
12164                }),
12165                y: Expr::Var(Number {
12166                    value: 0.0,
12167                    units: NumericSuffix::Mm,
12168                }),
12169            },
12170            end: Point2d {
12171                x: Expr::Var(Number {
12172                    value: 0.0,
12173                    units: NumericSuffix::Mm,
12174                }),
12175                y: Expr::Var(Number {
12176                    value: 5.0,
12177                    units: NumericSuffix::Mm,
12178                }),
12179            },
12180            center: Point2d {
12181                x: Expr::Var(Number {
12182                    value: 0.0,
12183                    units: NumericSuffix::Mm,
12184                }),
12185                y: Expr::Var(Number {
12186                    value: 0.0,
12187                    units: NumericSuffix::Mm,
12188                }),
12189            },
12190            construction: None,
12191        };
12192        let segment = SegmentCtor::Arc(arc_ctor);
12193        let (src_delta, scene_delta) = frontend
12194            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12195            .await
12196            .unwrap();
12197        assert!(
12198            src_delta
12199                .text
12200                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
12201            "Expected arc in source, got: {}",
12202            src_delta.text
12203        );
12204        assert!(!scene_delta.new_objects.is_empty());
12205
12206        ctx.close().await;
12207        mock_ctx.close().await;
12208    }
12209
12210    #[tokio::test(flavor = "multi_thread")]
12211    async fn test_extra_newlines_add_circle() {
12212        // Extra blank lines between settings and sketch.
12213        let initial_source = "@settings(defaultLengthUnit = mm)
12214
12215
12216
12217sketch001 = sketch(on = XY) {
12218}
12219";
12220
12221        let program = Program::parse(initial_source).unwrap().0.unwrap();
12222        let mut frontend = FrontendState::new();
12223
12224        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12225        let mock_ctx = ExecutorContext::new_mock(None).await;
12226        let version = Version(0);
12227
12228        frontend.hack_set_program(&ctx, program).await.unwrap();
12229        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12230        let sketch_id = sketch_object.id;
12231
12232        let circle_ctor = CircleCtor {
12233            start: Point2d {
12234                x: Expr::Var(Number {
12235                    value: 5.0,
12236                    units: NumericSuffix::Mm,
12237                }),
12238                y: Expr::Var(Number {
12239                    value: 0.0,
12240                    units: NumericSuffix::Mm,
12241                }),
12242            },
12243            center: Point2d {
12244                x: Expr::Var(Number {
12245                    value: 0.0,
12246                    units: NumericSuffix::Mm,
12247                }),
12248                y: Expr::Var(Number {
12249                    value: 0.0,
12250                    units: NumericSuffix::Mm,
12251                }),
12252            },
12253            construction: None,
12254        };
12255        let segment = SegmentCtor::Circle(circle_ctor);
12256        let (src_delta, scene_delta) = frontend
12257            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12258            .await
12259            .unwrap();
12260        assert!(
12261            src_delta
12262                .text
12263                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
12264            "Expected circle in source, got: {}",
12265            src_delta.text
12266        );
12267        assert!(!scene_delta.new_objects.is_empty());
12268
12269        ctx.close().await;
12270        mock_ctx.close().await;
12271    }
12272
12273    #[tokio::test(flavor = "multi_thread")]
12274    async fn test_extra_newlines_add_constraint() {
12275        // Extra newlines with a sketch containing two lines - add a coincident constraint.
12276        let initial_source = "@settings(defaultLengthUnit = mm)
12277
12278
12279
12280sketch001 = sketch(on = XY) {
12281  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
12282  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
12283}
12284";
12285
12286        let program = Program::parse(initial_source).unwrap().0.unwrap();
12287        let mut frontend = FrontendState::new();
12288
12289        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12290        let mock_ctx = ExecutorContext::new_mock(None).await;
12291        let version = Version(0);
12292        let project_id = ProjectId(0);
12293        let file_id = FileId(0);
12294
12295        frontend.hack_set_program(&ctx, program).await.unwrap();
12296        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12297        let sketch_id = sketch_object.id;
12298        let sketch = expect_sketch(sketch_object);
12299
12300        // Extract segment data before edit_sketch borrows frontend mutably.
12301        let line_ids: Vec<ObjectId> = sketch
12302            .segments
12303            .iter()
12304            .copied()
12305            .filter(|seg_id| {
12306                matches!(
12307                    &frontend.scene_graph.objects[seg_id.0].kind,
12308                    ObjectKind::Segment {
12309                        segment: Segment::Line(_)
12310                    }
12311                )
12312            })
12313            .collect();
12314        assert_eq!(line_ids.len(), 2, "Expected two line segments");
12315
12316        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
12317        let ObjectKind::Segment {
12318            segment: Segment::Line(line1_data),
12319        } = &line1.kind
12320        else {
12321            panic!("Expected line");
12322        };
12323        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
12324        let ObjectKind::Segment {
12325            segment: Segment::Line(line2_data),
12326        } = &line2.kind
12327        else {
12328            panic!("Expected line");
12329        };
12330
12331        // Build constraint before entering sketch mode.
12332        let constraint = Constraint::Coincident(Coincident {
12333            segments: vec![line1_data.end.into(), line2_data.start.into()],
12334        });
12335
12336        // Enter sketch edit mode.
12337        frontend
12338            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
12339            .await
12340            .unwrap();
12341        let (src_delta, _scene_delta) = frontend
12342            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12343            .await
12344            .unwrap();
12345        assert!(
12346            src_delta.text.contains("coincident("),
12347            "Expected coincident constraint in source, got: {}",
12348            src_delta.text
12349        );
12350
12351        ctx.close().await;
12352        mock_ctx.close().await;
12353    }
12354
12355    #[tokio::test(flavor = "multi_thread")]
12356    async fn test_extra_newlines_add_line_then_edit_line() {
12357        // Extra newlines after @settings - add a line, then edit it.
12358        let initial_source = "@settings(defaultLengthUnit = mm)
12359
12360
12361
12362sketch001 = sketch(on = XY) {
12363}
12364";
12365
12366        let program = Program::parse(initial_source).unwrap().0.unwrap();
12367        let mut frontend = FrontendState::new();
12368
12369        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12370        let mock_ctx = ExecutorContext::new_mock(None).await;
12371        let version = Version(0);
12372
12373        frontend.hack_set_program(&ctx, program).await.unwrap();
12374        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12375        let sketch_id = sketch_object.id;
12376
12377        // Add a line.
12378        let line_ctor = LineCtor {
12379            start: Point2d {
12380                x: Expr::Number(Number {
12381                    value: 0.0,
12382                    units: NumericSuffix::Mm,
12383                }),
12384                y: Expr::Number(Number {
12385                    value: 0.0,
12386                    units: NumericSuffix::Mm,
12387                }),
12388            },
12389            end: Point2d {
12390                x: Expr::Number(Number {
12391                    value: 10.0,
12392                    units: NumericSuffix::Mm,
12393                }),
12394                y: Expr::Number(Number {
12395                    value: 10.0,
12396                    units: NumericSuffix::Mm,
12397                }),
12398            },
12399            construction: None,
12400        };
12401        let segment = SegmentCtor::Line(line_ctor);
12402        let (src_delta, scene_delta) = frontend
12403            .add_segment(&mock_ctx, version, sketch_id, segment, None)
12404            .await
12405            .unwrap();
12406        assert!(
12407            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
12408            "Expected line in source after add, got: {}",
12409            src_delta.text
12410        );
12411        // Line creates start point, end point, and line segment.
12412        let line_id = *scene_delta.new_objects.last().unwrap();
12413
12414        // Edit the line.
12415        let line_ctor = LineCtor {
12416            start: Point2d {
12417                x: Expr::Number(Number {
12418                    value: 1.0,
12419                    units: NumericSuffix::Mm,
12420                }),
12421                y: Expr::Number(Number {
12422                    value: 2.0,
12423                    units: NumericSuffix::Mm,
12424                }),
12425            },
12426            end: Point2d {
12427                x: Expr::Number(Number {
12428                    value: 13.0,
12429                    units: NumericSuffix::Mm,
12430                }),
12431                y: Expr::Number(Number {
12432                    value: 14.0,
12433                    units: NumericSuffix::Mm,
12434                }),
12435            },
12436            construction: None,
12437        };
12438        let segments = vec![ExistingSegmentCtor {
12439            id: line_id,
12440            ctor: SegmentCtor::Line(line_ctor),
12441        }];
12442        let (src_delta, scene_delta) = frontend
12443            .edit_segments(&mock_ctx, version, sketch_id, segments)
12444            .await
12445            .unwrap();
12446        assert!(
12447            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
12448            "Expected edited line in source, got: {}",
12449            src_delta.text
12450        );
12451        assert_eq!(scene_delta.new_objects, vec![]);
12452
12453        ctx.close().await;
12454        mock_ctx.close().await;
12455    }
12456}